public void AddStatesForMovesForCar(BoardState state, byte car, int xVec, int yVec, List<BoardState> seenStates, List<BoardState> newStates)
        {
            // multiply vector by an increasing number to test all move options until the move is blocked. 
            // e.g. move left 1, move left 2, move left 3, etc
            int mul = 1;

            var direction =
                (xVec != 0) ?
                (xVec > 0 ? BoardState.Direction.Right : BoardState.Direction.Left) :
                (yVec > 0 ? BoardState.Direction.Down : BoardState.Direction.Up);

            while (true)
            {
                // reset here but only initalised after validations have been performed
                BoardState newState = null;

                for (int y = 0; y < state.Height; y++)
                {
                    for (int x = 0; x < state.Width; x++)
                    {
                        var curOfs = y * state.Width + x;

                        if (state.Board[curOfs] != car)
                        {
                            continue;
                        }

                        var nx = x + (xVec * mul);
                        var ny = y + (yVec * mul);

                        var newOfs = ny * state.Width + nx;

                        // invalid if the move has caused the piece to move off-board
                        if ((nx < 0) || (ny < 0) || (nx >= state.Width) || (ny >= state.Height)) return;

                        // if there's a matched piece to the left or right, we can't move up or down
                        if ((((x > 0) && (state.Board[curOfs - 1] == car)) || ((x < state.Width - 1) && (state.Board[curOfs + 1] == car))) && (yVec != 0)) return;

                        // if there's a matched piece up or down, we can't move to the left or right
                        if ((((y > 0) && (state.Board[curOfs - state.Width] == car)) || ((y < state.Height - 1) && (state.Board[curOfs + state.Width] == car))) && (xVec != 0)) return;

                        // invalid if the new position is not either empty or another of the same piece
                        if ((state.Board[newOfs] != BoardState.CELL_EMPTY) && (state.Board[newOfs] != car)) return;

                        // initialise after checks have been performed
                        if (newState == null)
                        {
                            newState = new BoardState(state);
                        }

                        if (newState.Board[curOfs] != BoardState.CELL_ACTIVE)
                        {
                            newState.Board[curOfs] = BoardState.CELL_EMPTY;
                        }
                        newState.Board[newOfs] = BoardState.CELL_ACTIVE;
                    }
                }

                // will crash with newState = null if the car character was not matched
                for (int i = 0; i < state.Board.Length; i++)
                {
                    if (newState.Board[i] == BoardState.CELL_ACTIVE)
                    {
                        newState.Board[i] = car;
                    }
                }

                // if this state has ever been seen before, continuing with this tree is pointless
                // scan backwards because it's very likely that a state not far back is the same rather than one at the beginning
                for (int i = seenStates.Count() - 1; i >= 0; i--)
                {
                    if (newState.Equals(seenStates[i]))
                    {
                        return;
                    }
                }

                // store the narration
                newState.Narration = GetNarration(car, mul, direction, newState.CarMap);

                seenStates.Add(newState);
                newStates.Add(newState);

                mul++;
            }

        }
        public string GetNarration(byte car, int distance, BoardState.Direction direction, Dictionary<byte, char> carMap)
        {
            var directionStr = "";

            switch (direction)
            {
                case BoardState.Direction.Up: directionStr = "↑"; break;
                case BoardState.Direction.Down: directionStr = "↓"; break;
                case BoardState.Direction.Left: directionStr = "←"; break;
                case BoardState.Direction.Right: directionStr = "→"; break;
            }

            return string.Format(
                "{0} {1} {2}",
                carMap[car],
                directionStr,
                distance);
        }
        public List<BoardState> AddStates(BoardState initialState, List<BoardState> seenStates)
        {
            var newStates = new List<BoardState>();

            // 1-based so we skip empty
            for (byte car = 1; car <= initialState.CarCount; car++)
            {
                this.AddStatesForMovesForCar(initialState, car, 1, 0, seenStates, newStates);
                this.AddStatesForMovesForCar(initialState, car, -1, 0, seenStates, newStates);
                this.AddStatesForMovesForCar(initialState, car, 0, 1, seenStates, newStates);
                this.AddStatesForMovesForCar(initialState, car, 0, -1, seenStates, newStates);
            }

            return newStates;
        }
        public BoardState Solve(BoardState initialState, int targetX, int targetY)
        {

            int maxDepth = 1;

            int i = 0;

            int targetOfs = targetY * initialState.Width + targetX;

            while (true)
            {
                Queue<BoardState> stack = new Queue<BoardState>();
                
                stack.Enqueue(initialState);

                List<BoardState> seenStates = new List<BoardState>();
                seenStates.Add(initialState);

                i = 0;

                Console.WriteLine("Searching at depth " + maxDepth + ".");

                while (stack.Count() > 0)
                {
                    var state = stack.Dequeue();

                    if (state.Board[targetOfs] == BoardState.CELL_REDCAR)
                    {
                        return state;
                    }

                    if (state.Depth <= maxDepth)
                    {
                        foreach (var newState in this.AddStates(state, seenStates))
                        {
                            stack.Enqueue(newState);

                            i++;
                        }
                    }
                }

                if (maxDepth > 0)
                {
                    Console.WriteLine("  Evaluated " + i + " valid leaves.");
                }

                if (i == 0)
                {
                    return null;
                }

                maxDepth++;
            }
        }
        public void RenderBoardToConsole(BoardState board)
        {
            Console.WriteLine();
            Console.WriteLine(board.Narration);

            for (int i = 0; i < board.Board.Length; i++)
            {
                if (i % board.Width == 0)
                {
                    Console.WriteLine();
                }

                if (board.Board[i] == 0)
                {
                    Console.Write(" ");
                }
                else {
                    Console.ForegroundColor = this.GetConsoleColorForIndex(board.Board[i]);
                    Console.Write("X");
                }
            }

            Console.ForegroundColor = ConsoleColor.Gray;
            Console.WriteLine();
        }
        public BoardState SetBoardFromText(string[] lines, char redCarChar = 'X', char emptyChar = '.')
        {
            var cx = 0;
            foreach (var line in lines)
            {
                if (line.Length > cx) cx = line.Length;
            }

            var board = new byte[lines.Length, cx];

            Dictionary<char, byte> map = new Dictionary<char, byte>();

            map.Add(emptyChar, BoardState.CELL_EMPTY);
            map.Add(redCarChar, BoardState.CELL_REDCAR);
            
            int y = 0;
            foreach (var line in lines)
            {
                int x = 0;
                foreach (var cell in line)
                {

                    if (!map.ContainsKey(cell))
                    {
                        // find a console colour
                        byte colour = 1;
                        while (map.ContainsValue(colour))
                        {
                            colour++;
                        }

                        if (colour > 0xf)
                        {
                            throw new Exception("Too many cars to render.");
                        }

                        map.Add(cell, colour);
                    }

                    board[y, x] = map[cell];
                    x++;
                }
                y++;
            }

            var boardState = new BoardState(board);

            boardState.CarMap = map.ToDictionary(item => item.Value, item => item.Key);

            return boardState;

        }
        public BoardState(BoardState parent)
        {
            this.Parent = parent;
            this.CarMap = parent.CarMap;

            this.Width = parent.Width;
            this.Height = parent.Height;
            this.CarCount = parent.CarCount;

            this.Depth = parent.Depth + 1;

            this.Board = new byte[parent.Board.Length];

            Buffer.BlockCopy(parent.Board, 0, this.Board, 0, parent.Board.Length);

        }