Hello! Welcome back to building our chess game in Rust.
In the last lesson, we successfully created a Board::new() function to initialize our board with all the pieces in their standard starting positions. However, when we printed the board using println!("{:?}", board), the output was a dense wall of text, which is hard to read.
Today, we'll fix that. Our learning outcome is to implement a method to print the board state to the console for debugging. We'll transform the messy Debug output into a clear, formatted grid that looks like a real chessboard. A good visual representation is one of the most powerful tools a game developer has for debugging.
This is the kind of output we're aiming for:

1. From Developer-Facing to User-Facing: The Display Trait
So far, we've used {:?} in our println! statements. This formatting marker tells Rust to use the Debug trait, which provides a developer-focused representation of a struct. To create a more user-friendly format, Rust provides another trait: Display.
Debug({:?}): For programmers. It's meant to show the full structure of a value for debugging. Most types can derive it automatically (#[derive(Debug)]).Display({}): For users. It defines a canonical, "pretty" way to print a type. You must implement it manually.
We will implement the Display trait for our Board struct. This will allow us to simply write println!("{}", board) and get a beautiful, formatted output.
2. A Strategy for Printing the Board
Implementing Display for our Board will involve a few steps:
- Define a character for each piece: We need a way to convert a
Piecestruct into a single character (e.g., a white pawn to 'P', a black knight to 'n'). We'll create a helper method on ourPiecestruct for this. - Iterate over the board: We'll write a nested loop to go through each rank and file of our
[[Option<Piece>; 8]; 8]array. - Build the output string: For each square, we'll check if it's
Some(piece)orNone. We'll append the correct character to a string, adding newline characters after each rank to create the grid format.
Let's see this logic in action.
Learn Rust and Build a Chesscomputer Ep.1: Board representation
Before we write our own code, let's watch a short segment from a video by CodeScope that demonstrates a very similar process. The author implements a to_string method, which is conceptually identical to what we'll do with Rust's Display trait. Pay close attention to how they iterate over the board and have a separate function to convert a piece into a character.
Watch from 00:28:44 to 00:33:01. Focus on the to_string method for the Game struct. Notice the loop over the squares, the check for an empty or occupied square, and the call to the piece_to_string helper function. This is the exact logic we will be implementing.
3. Hands-On: Implementing the Display Trait
Time to get coding. We'll start by defining the character representation for our pieces and then implement the Display logic for the board itself.
Step 1: Convert a Piece to a char
First, open your src/pieces.rs file. We'll add a method to the Piece struct to get its character representation. A common convention in text-based chess is to use uppercase for white pieces and lowercase for black pieces.
Add this impl block to src/pieces.rs:
// In src/pieces.rs
impl Piece {
/// Returns the character representation of the piece.
/// White pieces are uppercase, black pieces are lowercase.
pub fn to_char(&self) -> char {
let char = match self.kind {
PieceType::Pawn => 'p',
PieceType::Rook => 'r',
PieceType::Knight => 'n',
PieceType::Bishop => 'b',
PieceType::Queen => 'q',
PieceType::King => 'k',
};
if self.color == PieceColor::White {
char.to_ascii_uppercase()
} else {
char
}
}
}
Note: We use 'n' for Knight to avoid collision with 'k' for King.
Step 2: Implement Display for the Board
Now, let's make the Board printable. For this, we'll look at a great example from a Code Review Stack Exchange post, which demonstrates this exact pattern.
Generating and printing chess board in Rust
A post on the Code Review Stack Exchange provides an excellent, concise example of how to implement Display for a chess board. We will adapt their approach for our own Board struct.
In the first code block of the question, find the impl fmt::Display for Board block. Notice how it builds a String by iterating through the ranks and files, appending the symbol for each square, and adding a newline character (\n) after each rank. We will follow this structure.
Inspired by that example, open your src/board/mod.rs file.
- First, add
use std::fmt;to the top of the file to import the necessary formatting tools. - Then, add the following
implblock at the bottom of the file.
// In src/board/mod.rs
use std::fmt;
// ... other use statements like `use crate::pieces::{...}` should be here ...
// ... Board struct definition and Board::new() function ...
impl fmt::Display for Board {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut board_str = String::new();
for rank in self.pieces.iter() {
for square in rank.iter() {
match square {
Some(piece) => board_str.push(piece.to_char()),
None => board_str.push('.'),
}
board_str.push(' '); // Add a space for better alignment
}
board_str.push('\n'); // Newline after each rank
}
write!(f, "{}", board_str)
}
}
Step 3: Test Your New Display Implementation
It's time for the payoff. Go to your main function in src/main.rs.
Change the println! from this:println!("New board with starting pieces: {:?}", board);
To this:println!("{}\n", board);
Now, run cargo run in your terminal. You should see a beautifully formatted 8x8 grid representing your starting chessboard!
r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
P P P P P P P P
R N B Q K B N R
4. Enhancement: Using Unicode Chess Pieces
Our ASCII board is functional, but we can make it look even better using Unicode characters (♜, ♞, ♟, etc.). This is also a great chance to learn how to add an external library, or "crate," to your project.
We'll use the prettychars crate, which makes it easy to work with Unicode symbols.
prettychars: Unicode text styling
Let's explore the prettychars crate. It provides an easy way to look up Unicode symbols by name, including a full set of chess pieces.
First, read the 'Quick Start' section to see how to add the dependency to your Cargo.toml. Then, scroll down to the 'Glyph Categories' section and notice that 'Chess' is listed, confirming it has the symbols we need.
-
Add the dependency: Open your
Cargo.tomlfile and add the following line under[dependencies]:prettychars = "0.1" -
Update your
to_charmethod: We need to modify ourPiece::to_charmethod to return the Unicode symbol. Theprettychars::glyph()function looks up symbols by name. The names follow a predictable pattern like"chess.white.pawn".Go back to
src/pieces.rs. First, adduse prettychars::glyph;at the top. Then, update yourimpl Pieceblock to look like this:// In src/pieces.rs use prettychars::glyph; // ... your other structs and enums ... impl Piece { pub fn to_char(&self) -> char { let piece_name = match self.kind { PieceType::Pawn => "pawn", PieceType::Rook => "rook", PieceType::Knight => "knight", PieceType::Bishop => "bishop", PieceType::Queen => "queen", PieceType::King => "king", }; let color_name = if self.color == PieceColor::White { "white" } else { "black" }; let glyph_name = format!("chess.{}.{}", color_name, piece_name); // .unwrap_or('?') will return the symbol, or '?' if not found. glyph(&glyph_name).unwrap_or('?') } }
Now, run cargo run again. Your terminal should display the board with elegant Unicode chess pieces! If your terminal has trouble rendering them, you can always revert to the ASCII version. Most modern terminals will handle this perfectly.
Conclusion
Excellent work! You now have a clean, readable representation of your game state that you can print anytime. This will be incredibly useful for debugging piece movement, captures, and all the game logic we're about to build.
Key Takeaways:
- Rust's
Displaytrait is used for creating user-friendly, formatted text representations of your types. - You implement it using
impl fmt::Display for YourTypeand thewrite!macro. - Breaking down a problem into smaller parts (like a
piece.to_char()helper method) keeps your code clean and organized. - Adding external crates from
crates.iois as simple as adding a line to yourCargo.toml.
In our next lesson, we will begin the exciting task of making our pieces move. We'll start with the most numerous but complex piece on the board: the pawn. We'll write the logic to generate all valid moves for a pawn from any given square.