Let us Build Checkers, the Board Game in Rust

Reading Time: 4 minutes

In my previous blog, you have seen how to build checkers game and implements its basic rules using WebAssembly. In this blog, we build Checkers game using Rust programming language.

Let’s get started

To get started, type the following in the terminal:

$ cargo new --lib rustycheckers

Now rustycheckers the library project is created.

Setting Up the Checkers Board

To start writing the code to manage the game board, the first thing you’ll want to do is write some code to manage a GamePiece.

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum PieceColor {
    White,
    Black,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GamePiece {
    pub color: PieceColor,
    pub crowned: bool,
}

impl GamePiece {
    pub fn new(color: PieceColor) -> GamePiece {
        GamePiece {
            color,
            crowned: false,
        }
    }

    pub fn crowned(piece: GamePiece) -> GamePiece {
        GamePiece {
            color: piece.color,
            crowned: true,
        }
    }
}

This game piece has two functions:

  • new():- The new function creates a new game piece of a given color.
  • crowned():- The crowned function creates a new piece of a given color with a crown on top.

Next, you can create the concept of a Coordinate.

#[derive(Debug, Clone, PartialEq, Copy)]
pub struct Coordinate(pub usize, pub usize);

impl Coordinate {
    pub fn on_board(self) -> bool {
        let Coordinate(coor_x, coor_y) = self;
        coor_x <= 7 && coor_y <= 7
    }

    pub fn jump_targets_from(&self) -> impl Iterator<Item = Coordinate> {
        let mut jumps = Vec::new();
        let Coordinate(coor_x, coor_y) = *self;
        if coor_y >= 2 {
            jumps.push(Coordinate(coor_x + 2, coor_y - 2));
        }
        jumps.push(Coordinate(coor_x + 2, coor_y + 2));
        if coor_x >= 2 && coor_y >= 2 {
            jumps.push(Coordinate(coor_x - 2, coor_y - 2));
        }
        if coor_x >= 2 {
            jumps.push(Coordinate(coor_x - 2, coor_y + 2));
        }
        jumps.into_iter()
    }

    pub fn move_targets_from(&self) -> impl Iterator<Item = Coordinate> {
        let mut moves = Vec::new();
        let Coordinate(coor_x, coor_y) = *self;
        if coor_x >= 1 {
            moves.push(Coordinate(coor_x - 1, coor_y + 1));
        }
        moves.push(Coordinate(coor_x + 1, coor_y + 1));
        if coor_y >= 1 {
            moves.push(Coordinate(coor_x + 1, coor_y - 1));
        }
        if coor_x >= 1 && coor_y >= 1 {
            moves.push(Coordinate(coor_x - 1, coor_y - 1));
        }
        moves.into_iter()
    }
}
  • jump_targets_from():- The jump_targets_from function returns a list of potential jump targets from the given coordinate.
  • move_targets_from():- The move_targets_from function returns a list of potential move targets from the given current coordinate location.
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct Move {
    pub from: Coordinate,
    pub to: Coordinate,
}

impl Move {
    pub fn new(from: (usize, usize), to: (usize, usize)) -> Move {
        Move {
            from: Coordinate(from.0, from.1),
            to: Coordinate(to.0, to.1),
        }
    }
}

The Move struct represents a game move.

Implementing Checkers Game Rules

In the previous blog, we managed the game state by manipulating bytes with direct memory access. In WebAssembly we don’t have the privilege of a two-dimensional array but in Rust, we have this privilege.

use super::board::{Coordinate, GamePiece, Move, PieceColor};
pub struct GameEngine {
    board: [[Option<GamePiece>; 8]; 8],
    current_turn: PieceColor,
    move_count: u32,
}

pub struct MoveResult {
    pub move: Move,
    pub crowned: bool,
}

The GameEngine struct anchor all of the game engine functionality to the same place, and give you a spot to maintain the game state.

impl GameEngine {
    pub fn new() -> GameEngine {
        let mut engine = GameEngine {
            board: [[None; 8]; 8],
            current_turn: PieceColor::Black,
            move_count: 0,
        };
        engine.initialize_pieces();
        engine
    }

    pub fn initialize_pieces(&mut self) {
        [1, 3, 5, 7, 0, 2, 4, 6, 1, 3, 5, 7]
            .iter()
            .zip([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2].iter())
            .map(|(pt_a, pt_b)| (*pt_a as usize, *pt_b as usize))
            .for_each(|(coor_x, coor_y)| {
               self.board[coor_x][coor_y]= Some(GamePiece::new(PieceColor::White));
            });
        [0, 2, 4, 6, 1, 3, 5, 7, 0, 2, 4, 6]
            .iter()
            .zip([5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7].iter())
            .map(|(pt_a, pt_b)| (*pt_a as usize, *pt_b as usize))
            .for_each(|(coor_x, coor_y)| {
                self.board[coor_x][coor_y]=Some(GamePiece::new(PieceColor::Black));
            });
    }
}

In the engine’s constructor we have created a mutable instance of the GameEngine struct.

  • initialize_pieces():- The initialize_pieces function is used to set the board up for play.
  • iter():- The iter function converts an array of known x- or y-coordinate positions into an iterator.
  • zip():- The zip function merges two iterators into an iterator of tuples.
  • map():- The map function converts the coordinates from i32 to usize.

The &mut self parameter to initialize_pieces() indicates that it can only be used by a mutable reference to a game engine.

Let us move pieces on the board

pub fn move_piece(&mut self, mv: &Move) -> Result<MoveResult, ()> {
    let legal_moves = self.legal_moves();
    if !legal_moves.contains(move) {
        return Err(());
    }
    let Coordinate(from_coor_x, from_coor_y) = move.from;
    let Coordinate(to_coor_x, to_coor_y) = move.to;
    let piece = self.board[from_coor_x][from_coor_y].unwrap();
    let midpiece_coordinate = self.midpiece_coordinate(from_coor_x, from_coor_y, to_coor_x, to_coor_y);
    if let Some(Coordinate(coor_x, coor_y)) = midpiece_coordinate {
        self.board[coor_x][coor_y] = None; 
    }
    self.board[to_coor_x][to_coor_y] = Some(piece);
    self.board[from_coor_x][from_coor_y] = None;
    let crowned = if self.should_crown(piece, move.to) {
        self.crown_piece(move.to);
        true
    } else {
        false
    };
    self.advance_turn();
    Ok(MoveResult {
        move: move.clone(),
        crowned: crowned,
    })
}
  • move_piece():- The move_piece function computes the list of legal moves based on whose turn it is and the state of the game board.

The move_piece function returns Result. The result can have two values: either Ok or Err. Using a pattern match on a result is a clean way to handle and propagate errors back up the call stack.

fn legal_moves(&self) -> Vec<Move> {
    let mut moves: Vec<Move> = Vec::new();
    for col in 0..8 {
        for row in 0..8 {
            if let Some(piece) = self.board[col][row] {
                if piece.color == self.current_turn {
                    let loc = Coordinate(col, row);
                    let mut vmoves = self.valid_moves_from(loc);
                    moves.append(&mut vmoves);
                }
            }
        }
    }
    moves
}

fn valid_moves_from(&self, loc: Coordinate) -> Vec<Move> {
    let Coordinate(coor_x, coor_y) = loc;
    if let Some(piece) = self.board[coor_x][coor_y] {
        let mut jumps = loc
            .jump_targets_from()
            .filter(|target| self.valid_jump(&piece, &loc, &target))
            .map(|ref target| Move {
                from: loc.clone(),
                to: target.clone(),
            }).collect::<Vec<Move>>();
        let mut moves = loc
            .move_targets_from()
            .filter(|target| self.valid_move(&piece, &loc, &target))
            .map(|ref target| Move {
                from: loc.clone(),
                to: target.clone(),
            }).collect::<Vec<Move>>();
        jumps.append(&mut moves);
        jumps
    } else {
        Vec::new()
    }
}
  • legal_moves():- The legal_moves function loops through every space on the board and then computes a list of valid moves from that position.
  • valid_moves_from():- The valid_moves_from function produces a list of Move instances that are valid from the given coordinate.

In conclusion, I hope you got an idea to build a basic Checkers game using Rust programming language.

Thanks for reading !!

If you want to read more content like this?  Subscribe to Rust Times Newsletter and receive insights and latest updates, bi-weekly, straight into your inbox. Subscribe to Rust Times Newsletter: https://bit.ly/2Vdlld7.


Knoldus-blog-footer-image

Written by 

I am Software Consultant at Knoldus and I am curious about learning new technologies.