Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 36

Parsing Algebraic Notation

Recommended resources

Hello! Welcome back to our chess project.

In the last module, we put the finishing touches on our game's core logic. By implementing castling, en passant, and pawn promotion, your Board struct now fully understands the rules of chess. This is a huge achievement! However, our engine is still living in a silent world, only able to play out moves we hardcode.

Today, we begin a new module focused on making your project a playable game. Our first step is to bridge the gap between your game logic and a human player. This lesson's goal is to parse algebraic notation text input from the user into Move objects that our engine can understand. We'll take a simple string like "e2e4" and convert it into the Move struct your game logic is built upon.

1. The Language of Chess: UCI Notation

To communicate moves, chess programs need a standardized, unambiguous format. While you might see moves written as "e4" or "Nf3" (Standard Algebraic Notation), engines often use a more explicit format called UCI Long Algebraic Notation.

Chess engine, pt. 1: Getting started

To understand this format, let's read a brief introduction to the Universal Chess Interface (UCI) protocol from the article 'Chess engine, pt. 1: Getting started'.

First, read the section 'The UCI protocol' for a general overview. Then, skip down and read the section titled 'position'. Focus on the explanation of UCI long algebraic notation for standard moves, promotions, and castling.

As you just read, UCI long algebraic notation is beautifully simple and explicit:

  • A standard move is [from_square][to_square], like e2e4.
  • A promotion adds a character for the chosen piece, like e7e8q for promoting to a queen.
  • Castling is just a king move of two squares, like e1g1.

Our goal is to write a function that takes a string in this format and produces the Move struct we defined in the previous lesson:

pub struct Move {
    pub from: Position,
    pub to: Position,
    pub promotion: Option<PieceType>,
}

This means we need to convert text coordinates like e2 into our Position { file: 4, rank: 1 } struct.

Chessboard Algebraic Notation
This diagram shows the standard algebraic notation for a chessboard. Files 'a' through 'h' map to columns 0-7, and ranks '1' through '8' map to rows 0-7.

2. The Parsing Challenge

Parsing is the process of analyzing a string of symbols to determine its grammatical structure. In our case, it means taking a string like "e7e8q" and turning it into a structured Move object.

This process can fail. What if the user types "x9y0" or "hello"? A robust parser must handle these invalid inputs gracefully without crashing. This is where Rust's Result<T, E> enum is invaluable. Our parsing function will have a signature like this:
fn parse_move(input: &str) -> Result<Move, String>

It will either return Ok(Move) on success or Err(String) with an error message on failure.

Parse CLI Arguments With Rust Standard Crate 🦀📦 Rust Tutorial

Before we write the code, watch this short clip from the video 'Parse CLI Arguments With Rust' by Trevor Sullivan. It introduces the general concept of parsing strings into other data types and the importance of handling potential errors using Result.

Watch from 03:22 to 04:28 to understand the parse function and the need for error handling. Then, jump to 24:28 - 25:52 to see a practical example of checking if a parse operation was successful (is_ok) and returning a user-friendly error message, which is exactly what we need to do.


3. Hands-On: Building a Move Parser

Let's build our parser. A good place for this new logic would be in a new file. You could create src/input.rs and declare it as a module in src/main.rs with mod input;.

Our main task is to break down the input string into its components: from-square, to-square, and an optional promotion piece. A helper function to parse a single square (like "e2") will be very useful.

Step 1: Parse a Single Square

Let's write a function parse_square(s: &str) -> Result<Position, String>. It takes a 2-character string and should return a Position.

// In src/input.rs (or a suitable module)

// Assuming you have these defined elsewhere
use crate::{Position, PieceType, Move}; 

fn parse_square(s: &str) -> Result<Position, String> {
    let mut chars = s.chars();
    let file_char = chars.next().ok_or("Invalid square: missing file")?;
    let rank_char = chars.next().ok_or("Invalid square: missing rank")?;

    let file = match file_char {
        'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3,
        'e' => 4, 'f' => 5, 'g' => 6, 'h' => 7,
        _ => return Err(format!("Invalid file character: {}", file_char)),
    };

    // '1' has ASCII value 49. '1'.to_digit(10) gives 1. We subtract 1 for 0-indexing.
    let rank = rank_char.to_digit(10)
        .ok_or(format!("Invalid rank character: {}", rank_char))?
        .saturating_sub(1) as usize;

    if rank > 7 {
        return Err(format!("Invalid rank: {}", rank_char));
    }

    Ok(Position { rank, file })
}

This function:

  1. Pulls out the file and rank characters.
  2. Uses a match expression to convert the file character 'a'-'h' into a usize 0-7.
  3. Uses the to_digit(10) method to convert the rank character '1'-'8' into a number. We subtract 1 to align with our 0-indexed board representation.
  4. Returns a Result containing either the Position or an error String.

Step 2: Parse the Full Move String

Now we can use our helper function to build the main parser.

// In the same file as parse_square

pub fn parse_move(input: &str) -> Result<Move, String> {
    if input.len() < 4 || input.len() > 5 {
        return Err("Invalid move length. Use format e.g., 'e2e4' or 'e7e8q'".to_string());
    }

    let from = parse_square(&input[0..2])?;
    let to = parse_square(&input[2..4])?;

    let promotion = if input.len() == 5 {
        let promo_char = input.chars().nth(4).unwrap(); // Safe due to length check
        let piece_type = match promo_char.to_ascii_lowercase() {
            'q' => PieceType::Queen,
            'r' => PieceType::Rook,
            'b' => PieceType::Bishop,
            'n' => PieceType::Knight,
            _ => return Err(format!("Invalid promotion piece: {}", promo_char)),
        };
        Some(piece_type)
    } else {
        None
    };

    Ok(Move { from, to, promotion })
}

This function:

  1. Checks for a valid input length (4 or 5 characters).
  2. Uses string slicing (&input[0..2]) to get the "from" and "to" parts.
  3. Calls parse_square for each part. The ? operator neatly propagates any errors.
  4. If the string has a 5th character, it parses it as a promotion piece.
  5. Finally, it assembles and returns the Move struct.

Step 3: Test Your Parser

Now you can test your new function. In your main function, you can add a simple test:

// in main.rs
mod input; // If you created src/input.rs

fn main() {
    let test_inputs = vec!["e2e4", "g1f3", "a7a8q", "e1g1", "bogo"];

    for input in test_inputs {
        match input::parse_move(input) {
            Ok(mv) => println!("'{}' -> Parsed move: {:?}", input, mv),
            Err(e) => println!("'{}' -> Error: {}", input, e),
        }
    }
    
    // You can remove this test code later and start building your game loop!
}

Running this should give you a clear idea if your parser is working as expected!

Console Chess Game with Algebraic Notation Input
This is what we're building towards: a console interface where the user can input a move in algebraic notation and see the board update.

4. A Note on Using Crates

You've just built a parser from scratch, which is fantastic for understanding the mechanics. In the real world, for a complex domain like chess, developers often leverage existing, well-tested libraries (called "crates" in Rust).

Owlchess — Rust parser // Lib.rs

For example, the owlchess crate provides a very simple way to achieve what we just did. This is not for you to implement now, but to see how the Rust ecosystem can accelerate development.

Look at the code under the heading 'Making moves from UCI notation'. Notice the line Move::from_uci_legal("e2e4", &board).unwrap(). This single line accomplishes the parsing we just built.

Seeing this shows the power of leveraging community work. By building your own parser first, you now have a much deeper appreciation for what that function does under the hood. For our project, we'll stick with our own implementation as it's a great learning experience and keeps our dependencies minimal.


Conclusion

Excellent work! You have successfully built a crucial component that connects your game logic to the outside world. This parser is the "ears" of your chess engine, listening for player commands.

Key Takeaways:

  • UCI Long Algebraic Notation (e2e4, e7e8q) is an explicit and computer-friendly format for describing chess moves.
  • Parsing strings into structured data is a common programming task that requires careful handling of potential errors.
  • Rust's Result<T, E> enum and the ? operator are the perfect tools for writing robust parsing logic that doesn't crash on invalid input.
  • We broke the problem down by creating a helper function (parse_square) and then composing it into a larger function (parse_move).

Preview of the Next Lesson

Now that we can understand user commands, we need a way to orchestrate the game. In the next lesson, we will create a main game loop that alternates turns between players, asking for input, using our new parser to understand it, and updating the game state.