Skip to main content

Welcome to a Grasp lesson

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

Lesson 48

Moving Pieces with Clicks

Recommended resources

Hello! Welcome back to building our graphical chess game.

In our last lesson, you brought the game to life by allowing a player to click on a piece and see all of its valid moves highlighted. This created a clear visual language for a player's intentions.

Today, we'll close that loop. We will take the user's next click and, if it's on a valid destination square, execute the move. This will involve updating our core GameState and making the game truly interactive.

Our learning outcome is to execute a move when the player clicks a valid destination square for a selected piece.

1. Recap: The State of Play

As a quick reminder, your main function now holds the critical state for our UI:

let mut selected_piece: Option<(usize, usize)> = None;
let mut valid_moves: Vec<(usize, usize)> = Vec::new();
  • selected_piece: Stores the position of the piece the player has selected.
  • valid_moves: Contains all legal destination squares for that piece.

Our task is to use this state to interpret the player's next click.


2. A Clean Pattern for State Changes

Before diving into the code, let's consider a powerful pattern for managing game logic. A core idea in many applications, especially games, is that the game's state progresses through a series of discrete events. A player's move is a perfect example of such an event.

This concept is often called the reducer pattern: a function takes the current state and an event, and from them, produces a new state.

function reducer(current_state, event) -> new_state

In our chess game:

  • Current State: Your GameState struct (containing the board, current turn, etc.).
  • Event: A move, like "move piece from e2 to e4".
  • New State: The GameState after the piece has been moved.

Your execute_move method from Module 6 is, in fact, a reducer! To better understand this powerful and clean way of thinking about game logic, I recommend reading the following article.

Making a turn-based multiplayer game in Rust

The article 'Making a turn-based multiplayer game in Rust' provides an excellent explanation of the reducer pattern. While it discusses networking, the core principles of state management are directly applicable to our single-player game. It will help formalize the concepts we're about to implement.

Please read the sections 'A pattern for online turn-based games' and 'How do we write a reducer in Rust?'. Focus on how an 'event' (like a move) is used to transition a 'GameState' to its next version via a 'reduce' function. This is exactly what we're doing when we call game_state.execute_move().


3. Updating the Input Handler to Execute Moves

Now, let's apply this thinking to our input handling logic. We need to modify the if is_mouse_button_pressed... block from the last lesson. The new logic will prioritize executing a move if a piece is already selected.

The flow will be:

  1. If a piece is selected, check if the click is on a valid move square.
  2. If it is, execute the move, update the game state, and clear the selection.
  3. If it's not a valid move (or if no piece was selected), fall back to the selection logic from the previous lesson: try to select a piece at the clicked square.

Here is the updated logic. You can replace your entire if is_mouse_button_pressed... block with this more comprehensive version.

// In your main loop, replacing the previous input handler
if is_mouse_button_pressed(MouseButton::Left) {
    let (mouse_x, mouse_y) = mouse_position();
    let file = (mouse_x / SQUARE_SIZE).floor() as usize;
    let rank = (mouse_y / SQUARE_SIZE).floor() as usize;

    if rank < 8 && file < 8 {
        let mut move_executed = false;

        // --- Part 1: Try to execute a move ---
        if let Some(from_pos) = selected_piece {
            if valid_moves.contains(&(rank, file)) {
                // It's a valid move! Execute it on the game state.
                let to_pos = (rank, file);
                
                // You already built this function in Module 6!
                game_state.execute_move(from_pos, to_pos); 

                println!("Move executed from {:?} to {:?}", from_pos, to_pos);

                // Clear the UI state for the next turn
                selected_piece = None;
                valid_moves.clear();
                move_executed = true;
            }
        }

        // --- Part 2: If no move was made, try to select/deselect a piece ---
        if !move_executed {
            // Check if the square contains a piece of the current player's color
            if let Some(piece) = game_state.board[rank][file] {
                // This assumes your GameState has a `current_turn` field
                if piece.color == game_state.current_turn { 
                    // Select the new piece
                    selected_piece = Some((rank, file));
                    // And generate its moves
                    valid_moves = game_state.get_valid_moves(rank, file);
                } else {
                    // Clicked on an opponent's piece (but not a valid move)
                    // or another invalid square. Deselect.
                    selected_piece = None;
                    valid_moves.clear();
                }
            } else {
                // Clicked an empty square. Deselect.
                selected_piece = None;
                valid_moves.clear();
            }
        }
    } else {
        // Clicked outside the board. Deselect everything.
        selected_piece = None;
        valid_moves.clear();
    }
}

This logic is robust: it handles moving, selecting, re-selecting, and deselecting all in one block. Notice the call to game_state.execute_move(from_pos, to_pos). This is where your UI logic "dispatches" the move event to your backend game engine!


4. Revisiting the Bevy Tutorial

The Bevy chess tutorial you reviewed last lesson demonstrates this exact flow, but within an Entity-Component-System (ECS) architecture. Seeing the same logic in a different structure can deepen your understanding.

Chess game in Rust using Bevy

Let's revisit the 'Chess game in Rust using Bevy' tutorial. This time, focus on how it handles move execution and taking pieces after a selection has been made.

Please review the following sections: 'Movement': In the select_square system, notice the if let Some(selected_piece_entity) = selected_piece.entity check. Inside, it updates the piece's coordinates (piece.x = square.x). This is the Bevy equivalent of our game_state.execute_move() call. 'Implementing available squares': At the end of this section, the code is updated to call piece.is_move_valid(...) before changing the coordinates. This directly corresponds to our if valid_moves.contains(...) check. 'Taking pieces': This section adds logic to despawn an opponent's piece. Your execute_move function should already handle this by updating the board array (e.g., setting the captured piece's square to None).

Chess Knight Movement and Coordinate Mapping
In the last lesson, you implemented this "before" state. Now, when the user clicks one of the blue-dashed squares, the knight will move there.

Conclusion

Fantastic work! You have now implemented the complete gameplay cycle: a player selects a piece, sees their options, and clicks a destination to make their move. The user's input now directly modifies the core GameState, making the game playable from a logical standpoint.

Key Takeaways:

  • The input handling logic was extended to have two primary modes: executing a move (if a piece is selected) and selecting a piece (if no move is executed).
  • The reducer pattern provides a clean mental model where UI events (like clicks) are translated into game events (like moves) that update the central GameState.
  • A successful move requires updating the GameState and then resetting the UI state (selected_piece, valid_moves) to prepare for the next player's action.

Preview of the Next Lesson

When you run your code now, you should see the pieces "jump" to their new positions on the next frame. This happens because your rendering function reads from the GameState that you just updated. In the next lesson, we will officially address the learning outcome of updating the graphical display to reflect the new board state after a move. We will confirm that this redraw is working as expected and ensure our visual layer is perfectly in sync with our game's logic.