SAN to Coordinate Notation in C#

What is SAN?

Standard algebraic notation is a way to represent moves in a chess game. For instance the white rook in the bottom left of the chess board is square a1. If it moves up 1 square it is at a2. To represent the move in coordinate notation, you would write a1a2. Coordinate notation is easy for computers to read, but it gets a little messy for a human to look at entire games like that.

In standard algebraic notation the move might be written as Ra2 (Rook to square a2). What makes this conversion difficult is you need to generate all valid moves before you know the real source square. If there was another white rook on the board, you would first have to check to which rook was allowed to move to square a2.

When is it used?

If you are parsing a PGN file you'll need to make this conversion at some point. Loading games from PGN files was a very useful tool for debugging and testing game endings.

The Code.

static Dictionary fileToInt = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 }, { "d", 4 }, { "e", 5 }, { "f", 6 }, { "g", 7 }, { "h", 8 } };

static Move ConvertSANtoCoordinates(Board b, PieceType p, string san_token)
{
    string firstFile = "";
    string firstRank = "";
    string secondFile = "";
    string secondRank = "";
    string files = "abcdefgh";
    string ranks = "12345678";
    string promotes = "RNB";
    PieceType promote = PieceType.Queen;
    int destFile = -1;
    int destRank = -1;
    int sourceFile = -1;
    int sourceRank = -1;

    // castles are easy, so we check for them first
    if (t == "O-O")
    {
        //king side castle
        if (b.WhosTurn == PlayerColor.Black)
        {
            destRank = 8;
            destFile = 7;
        }
        else
        {
            destRank = 1;
            destFile = 7;
        }
    }
    else if (t == "O-O-O")
    {
        //queen side castle
        if (b.WhosTurn == PlayerColor.Black)
        {
            destRank = 8;
            destFile = 3;
        }
        else
        {
            destRank = 1;
            destFile = 3;
        }
    }
    else
    {
        // a non castling move, so interate through the chars in the SAN token
        for (int i = 0; i < san_token.Length; i++)
        {
            string st = san_token.Substring(i, 1);
            
            // is this character a valid file?
            if (files.Contains(st))
            {
                // is this the first time the token is referencing a file?
                if (firstFile == "")
                    firstFile = st;
                else
                    secondFile = st;
            }

            // is this character a valid rank?
            if (ranks.Contains(st))
            {
                // is this the first time the token is referencing a rank?
                if (firstRank == "")
                    firstRank = st;
                else
                    secondRank = st;
            }

            // is this character representing a promotion?
            if (promotes.Contains(st))
            {
                if (st == "N")
                {
                    promote = PieceType.Knight;
                }
                else if (st == "B")
                {
                    promote = PieceType.Bishop;
                }
                else if (st == "R")
                {
                    promote = PieceType.Rook;
                }
            }
        }

        //now figure out the source and dest rank and file based on what was in the SAN token
        if (secondFile == "")
        {
            destFile = fileToInt[firstFile];
        }
        else
        {
            sourceFile = fileToInt[firstFile];
            destFile = fileToInt[secondFile];
        }

        if (secondRank == "")
        {
            destRank = Convert.ToInt16(firstRank);
        }
        else
        {
            sourceRank = Convert.ToInt16(firstRank);
            destRank = Convert.ToInt16(secondRank);
        }
    }

    if ((destRank == -1) && (destFile == -1))
        throw new Exception("destination square is ambiguous");

    // get a list of all valid moves on the board
    ValidMoveEntry e = b.FindValidMoves();
    List moves = e.moves;
    
    Move nm = null; // this will store the coordinate notiation that we will return
    
    //iterate through the available moves and try to find one that matches
    //the source piece specified in the SAN token and one that matches
    //the source and dest rank & file if we have them

    foreach (Move m in moves)
    {
        int mdRank = 8 - (m.To / 8);
        int mdFile = (m.To % 8) + 1;

        if (p == b.GetPieceType(m.From))
        {
            if (mdFile == destFile)
            {
                if (mdRank == destRank)
                {
                    // The source piece type and the dest rank and file match
                    // skip this move if the sourceRank or sourceFiles have been specified in the SAN and they don't
                    // match.
                    int msRank = 8 - (m.From / 8);
                    int msFile = (m.From % 8) + 1;
                    if (sourceRank > -1)
                        if (sourceRank != msRank)
                            continue;

                    if (sourceFile > -1)
                        if (sourceFile != msFile)
                            continue;
                    // make sure the promotion matches too
                    if (m.PromoteType != 0)
                    {
                        if ((byte)promote != m.PromoteType)
                            continue;
                    }

                    //try the move just to make sure it's valid
                    MoveResult r = b.MakeMove(m);
                    if ((r & MoveResult.Invalid) != MoveResult.Invalid)
                    {
                        if ((r & MoveResult.PromotePawn) != MoveResult.PromotePawn)
                            b.UnmakeMoveAI(m.From, m.To);

                        // we already found a move, throw an exception
                        if (nm != null)
                            throw new Exception("Move was not unique");

                        nm = new Move((byte)m.From, (byte)m.To, (byte)promote);
                    }
                }
            }
        }
    }

    if (nm == null)
        throw new Exception("specified move not found on the board");

    return nm;
}