Skip to main content

Welcome to a Grasp lesson

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

Lesson 17

Structuring Board Positions with Rank and File

Recommended resources

Hello! In our last lesson, we built a solid data model for our chess pieces using enums for PieceType and PieceColor, and a struct to combine them into a Piece. This was a crucial step in defining the "what" of our game.

Today, we'll define the "where." A chess game happens on an 8x8 grid, and we need a way to represent every single square on that board. This lesson will focus on just that: modeling a board position. By the end, you'll have created a Square struct to precisely represent any of the 64 squares, moving us one step closer to building the full game board.

1. Modeling Coordinates: From 'e4' to Data

In chess, we refer to squares using algebraic notation like 'a1', 'h8', or 'e4'. For a computer program, it's more practical to represent these positions using numerical coordinates, like on a grid. A file (column) and a rank (row) are all we need to uniquely identify a square.

Chess Board Coordinates and Knight Movement
This image shows how a standard chess square like D5 can be mapped to numerical coordinates. Here, it's shown as (3,3), which is a common way to think about it in a 0-indexed system (where 'a' is column 0 and rank '1' is row 0).

Since a square's position is composed of two related pieces of data (rank and file), a struct is the perfect tool in Rust to group them together. While a tuple like (u8, u8) could work, using a struct with named fields (rank and file) makes our code far more readable and less error-prone. This video explains why grouping data with named fields is so valuable.

Structs in Rust

This segment from 'Structs in Rust' by Let's Get Rusty effectively demonstrates why structs are better than tuples for clarity. It refactors a program that calculates a rectangle's area, showing how naming the 'width' and 'height' fields improves readability.

Watch from 07:06 to 10:04. Pay attention to the progression from separate variables, to a tuple, and finally to a struct. Notice how the final version using a struct is the clearest.


2. Defining the Square Struct

Now that we see the value of using a struct, let's define one for our Square. We need two fields: rank and file. A good data type for these fields is u8, an 8-bit unsigned integer, which can hold values from 0 to 255. Since our board is 8x8, we only need values from 0 to 7, making u8 a perfect and efficient fit.

The official Rust documentation provides a clear guide on defining and using structs.

Defining and Instantiating Structs

Please read the following sections from 'The Rust Programming Language' book. They cover the essential syntax for defining a struct and creating an instance of it.

Read the sections 'Defining and Instantiating Structs' and the text that follows, up to the build_user function example. Focus on the struct keyword, the curly braces for the fields, and the dot notation (.) for accessing values.

Based on that, our Square struct will look like this:

pub struct Square {
    pub rank: u8, // Will represent rows 0-7
    pub file: u8, // Will represent columns 0-7
}

3. Hands-On: Integrating the Square

It's time to add this to our project. Since a Square is a fundamental part of the board, it makes sense to create a new board module to house it.

  1. Create the board module:

    • In your project's src directory, create a new folder named board.
    • Inside src/board, create a new file named mod.rs.
  2. Define the Square struct:
    Open src/board/mod.rs and add the following code. We'll also use the #[derive] attribute, just as we did for our Piece struct.

    • Copy and Clone: A Square is simple data. It's cheaper to copy it than to manage complex ownership rules with borrowing.
    • PartialEq: This allows us to compare two Square instances with ==, which will be essential for checking if a piece has reached a target square.
    • Debug: Lets us print the struct for debugging purposes.
    // src/board/mod.rs
    
    
    
    
    #[derive(Debug, Copy, Clone, PartialEq)]
    pub struct Square {
        pub rank: u8, // 0-7, mapping to ranks 1-8
        pub file: u8, // 0-7, mapping to files a-h
    }
    
  3. Register the new module:
    Open src/main.rs and tell Rust about our new board module, right alongside the pieces module.

    // src/main.rs
    
    mod board;
    mod pieces;
    
    // ... rest of the file
    
  4. Test your new struct:
    Let's make sure it works. In src/main.rs, import Square and create an instance of it inside your main function.

    // src/main.rs
    
    mod board;
    mod pieces;
    
    // Bring the new Square struct into scope
    use board::Square;
    use pieces::{Piece, PieceColor, PieceType};
    
    fn main() {
        // Create an instance of a Piece (from last lesson)
        let white_pawn = Piece {
            color: PieceColor::White,
            piece_type: PieceType::Pawn,
        };
        println!("Created piece: {:?}", white_pawn);
    
        // Create an instance of our new Square struct!
        // Let's represent the 'e4' square. In a 0-indexed grid,
        // this is rank 3 and file 4.
        let e4 = Square { rank: 3, file: 4 };
        println!("Created square: {:?}", e4);
    
        // We can even check for equality now!
        let also_e4 = Square { rank: 3, file: 4 };
        if e4 == also_e4 {
            println!("The squares are the same!");
        }
    }
    
  5. Run your code:
    Execute cargo run in your terminal. You should see the debug output for your piece and your new square, confirming that everything is set up correctly.

You may recall that in the last lesson, we added use super::board::Square; to src/pieces/mod.rs. At the time, it didn't work because the board module didn't exist. Now that we've created it, that line should compile without any issues, neatly connecting our modules.


Conclusion

Great work! You have now successfully modeled the two most fundamental components of our game: the Piece and the Square. By creating a dedicated Square struct, you've made the code more expressive and paved the way for building the rest of the game's logic on a solid, readable foundation.

Key Takeaways:

  • A struct with named fields (rank, file) is an excellent way to represent a board position, improving clarity over alternatives like tuples.
  • u8 is a suitable and efficient data type for 0-indexed board coordinates (0-7).
  • Organizing code into modules (board, pieces) makes the project structure clean and scalable.
  • Deriving traits like Copy, Clone, and PartialEq on simple data structs is a powerful Rust feature that simplifies coding.

In the next lesson, we will put these two pieces together. You will learn how to represent the entire 8x8 chessboard by creating a Board struct that holds an array of squares. We'll also tackle a key question: how do you represent a square that is empty? The answer lies in a very important Rust enum: Option<T>.