Hello! Welcome back.
In our last lesson, you implemented the crucial logic for detecting checkmate and stalemate. With that, all the core rules of chess are now encoded in our program. You have all the individual components: a board, piece movement, move validation, and game-over detection.
Today, we'll focus on the final learning outcome for this module: to assemble all components into a playable two-player chess game in the console. This lesson is about integration and polish. We'll structure our code to create a clean, interactive experience, turning our collection of functions into a finished product you can play from the command line.
This will be the capstone for the console version of our game, setting a solid foundation before we venture into graphics.
1. The Game Loop: A Blueprint for Interactive Programs
Your main function currently has a loop that manages the game flow. This is great, but as programs grow, a single, monolithic loop can become hard to read and manage. In game development, we often use a more structured pattern called the Game Loop.
A classic game loop breaks each cycle into three distinct phases:
- Process Input: Handle any input from the user (like keyboard presses or mouse clicks).
- Update Game State: Change the game's internal data based on the rules and the user's input (e.g., move a piece).
- Render: Draw the current game state to the screen.

This pattern helps organize code by separating concerns: I/O, logic, and drawing are all handled in different places.
To get a deeper feel for this concept, let's watch a short video that explains it well. While it discusses topics like frame rates, which are more critical for real-time games, the core principle of separating update logic from rendering is universal and will be essential when we build our GUI.
This video from Vittorio Romeo provides an excellent overview of the game loop concept. We'll focus on the general structure he presents.
Watch from 03:46 to 06:47. Pay attention to how the loop is structured to first accumulate time (or in our case, wait for input), then update the game state, and finally render the result.
2. Hands-On: Refactoring to a Structured Game Loop
Let's apply this pattern to our chess game. We'll refactor your main.rs to be cleaner and more organized. The goal is to make the main loop itself very simple, with the details handled by dedicated functions.
Here is an example of what a final, polished console chess game can look like. This will be our target for the render phase.

Step 1: Create a render_game Function
First, let's group all our printing logic into a single render function. This function will be responsible for clearing the screen and drawing the entire game state.
In src/main.rs, add a new function outside of your main function:
// In src/main.rs
// We'll use the Game and GameResult enums from the previous lesson
// ...
fn render_game(game: &Game) {
// Clear the console screen.
// The command is "cls" on Windows and "clear" on macOS/Linux.
if cfg!(target_os = "windows") {
std::process::Command::new("cmd").args(["/C", "cls"]).status().unwrap();
} else {
std::process::Command::new("clear").status().unwrap();
}
// Print the board
game.board.print_board(); // You should already have this method
println!("-----------------");
// Print the game status
match game.result {
GameResult::InProgress => {
println!("Current turn: {:?}", game.current_turn);
print!("Enter your move (e.g., e2e4): ");
// We need to flush stdout to make sure the prompt appears before input is read
use std::io::{self, Write};
io::stdout().flush().unwrap();
}
GameResult::Checkmate(winner) => {
println!("CHECKMATE! {:?} wins!", winner);
}
GameResult::Stalemate => {
println!("STALEMATE! The game is a draw.");
}
}
}
Step 2: Create a get_player_input Function
Next, let's isolate the code that reads from the console. This function will handle reading a line of text from the user and returning it as a String.
// In src/main.rs
use std::io;
fn get_player_input() -> String {
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
input.trim().to_string()
}
Step 3: Assemble the Final main Loop
Now, we can rewrite the main function to use our new, organized structure. The loop becomes much simpler and easier to understand.
// In src/main.rs
fn main() {
let mut game = Game::new(); // Assumes Game::new() sets up the board
loop {
// 1. RENDER PHASE
render_game(&game);
// If the game is over, print the final state and break the loop.
if game.result != GameResult::InProgress {
break;
}
// 2. INPUT PHASE
let input_str = get_player_input();
// 3. UPDATE PHASE
match input::parse_move(&input_str) {
Ok(parsed_move) => {
// Check if the move is legal for the current player
if game.is_move_legal(&parsed_move) {
game.board.execute_move(&parsed_move);
game.switch_turn();
// After the move, update the game state for the *next* player
game.update_game_state();
} else {
println!("\nIllegal move. It's either not a valid move or it leaves your king in check. Press Enter to continue.");
get_player_input(); // Pause for user to read the message
}
}
Err(e) => {
println!("\nInvalid move format: {}. Press Enter to continue.", e);
get_player_input(); // Pause for user to read the message
}
}
}
println!("Thanks for playing!");
}
And with that, you have a complete, playable two-player chess game running in your console! It has a clean structure that clearly separates rendering, input, and game logic—a structure that will serve you well as we move to the next stage of the project.
3. Exploring a Finished Console Chess Project
Now that you've assembled your own game, it can be very insightful to see how others have approached the same problem. chessterm is an open-source chess game written in Rust that runs in the terminal, much like yours. It has a more advanced user interface and uses a different board representation (bitboards), but its core purpose is the same.
chessterm: A Rust-powered chess engine in a terminal
Take a look at the README for the chessterm project. It gives a great overview of the author's journey and technical choices. This isn't for you to copy, but to get inspiration and see another developer's perspective.
Read the sections 'Why I Built This', 'Human vs Human Only', and 'Installation'. Then, if you're curious, I encourage you to follow the installation instructions to clone the repository and run it with cargo run. Seeing another finished console game in action can be very motivating.
Exploring projects like chessterm or RustChess (resource LINK) is a great way to learn. You can see different coding styles, discover new libraries, and get ideas for features you might want to add to your own projects later.
Conclusion
Congratulations! You have successfully assembled a fully playable, two-player chess game in the console. You've brought together everything from board representation and piece movement to complex rule validation and game-over logic into a single, cohesive application.
Key Takeaways:
- The Game Loop (
Input,Update,Render) is a powerful pattern for organizing interactive applications. - Refactoring logic into separate functions (
render_game,get_player_input) makes the main loop cleaner and easier to understand. - A polished console application involves more than just logic; it includes clear user prompts, error messages, and screen management.
- You have now completed a significant milestone: a feature-complete console chess engine.
Preview of the Next Lesson
With the logic of our chess engine complete and robust, it's time to give it a visual identity. In the next module, we'll dive into the world of graphics. We will start by adding an external graphics library (macroquad) as a dependency and learning how to create a game window and set up a main rendering loop. The game loop structure you created today will be directly applicable as we replace our console render_game function with one that draws to a graphical window.