4
\$\begingroup\$

This is the beginning of a chess program I'm writing in Rust. I'm new to Rust, so while this program works, I'm sure there are many improvements that could be made. For instance, I wanted to declare the squares variable as a fixed sized array of 64 square structs, rather than using a vector, but I couldn't get that to work, I think because I'm using a trait object.

First is the code within lib.rs.

mod piece;

use std::fmt;
use piece::*;

struct Square {
    piece: Option<Box<Piece>>
}

impl Square {
    pub fn new() -> Square {
        Square { piece: None }
    }

    fn symbol(&self) -> &str {
        match self.piece {
            Some(ref p) => p.symbol(),
            None => "."
        }
    }
}

impl fmt::Display for Square {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.symbol())
    }
}

pub struct Board {
    squares: Vec<Square>
}

impl Board {
    pub fn new() -> Board {
        let mut squares: Vec<Square> = Vec::new();
        let p = vec![(0, 1), (7, 6)];

        for _ in 0..64 { squares.push(Square::new()); }

        for pos in p {
            squares[pos.0 * 8 + 0].piece = Some(Box::new(Rook {}));
            squares[pos.0 * 8 + 1].piece = Some(Box::new(Knight {}));
            squares[pos.0 * 8 + 2].piece = Some(Box::new(Bishop {}));
            squares[pos.0 * 8 + 3].piece = Some(Box::new(Queen {}));
            squares[pos.0 * 8 + 4].piece = Some(Box::new(King {}));
            squares[pos.0 * 8 + 5].piece = Some(Box::new(Bishop {}));
            squares[pos.0 * 8 + 6].piece = Some(Box::new(Knight {}));
            squares[pos.0 * 8 + 7].piece = Some(Box::new(Rook {}));

            for i in (pos.1 * 8)..(pos.1 * 8 + 8) { 
                squares[i].piece = Some(Box::new(Pawn { en_passant: false })) 
            }
        }

        Board {
            squares: squares
        }
    }

    pub fn valid_moves() {

    }
}

impl fmt::Display for Board {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut res = String::new();
        for i in 0..8 {
            for j in 0..8 {
                res.push_str(self.squares[(i * 8) + j].symbol());
            }
            res.push_str("\n");
        }
        write!(f, "{}", res)
    }
}

Here is the code that defines the pieces (not much to this yet).

pub trait Piece {
    fn symbol(&self) -> &str;
    fn valid_moves(&self);
}

pub struct King;
impl Piece for King {
    fn symbol(&self) -> &str {
        "K"
    }

    fn valid_moves(&self) {

    }
}

pub struct Queen;
impl Piece for Queen {
    fn symbol(&self) -> &str {
        "Q"
    }

    fn valid_moves(&self) {

    }
}

pub struct Rook;
impl Piece for Rook {
    fn symbol(&self) -> &str {
        "R"
    }

    fn valid_moves(&self) {

    }
}

pub struct Bishop;
impl Piece for Bishop {
    fn symbol(&self) -> &str {
        "B"
    }

    fn valid_moves(&self) {

    }
}

pub struct Knight;
impl Piece for Knight {
    fn symbol(&self) -> &str {
        "k"
    }

    fn valid_moves(&self) {

    }
}

pub struct Pawn { pub en_passant: bool }
impl Piece for Pawn {
    fn symbol(&self) -> &str {
        "p"
    }

    fn valid_moves(&self) {

    }
}

I'd love to know if there are ways I could improve on this.

\$\endgroup\$
  • \$\begingroup\$ I may come back to this in a more thorough way later, but an important point first. Rust encourages functional over object oriented (imo, at least). If you ever use a box the very first question you should have is "does this need to be its own object". In this case, you may be better off keeping the pieces as Enums containing a moveset. You may find this causes less wrestling with the borrow checker later, as the piece is simply a bit of information on the square instead of another object contained within it. \$\endgroup\$ – Turksarama Feb 19 '18 at 5:51
2
\$\begingroup\$

You can define your pieces as an enum, then you don't need to box them in your squares:

#[derive(Clone, Copy)]
pub enum Piece {
    King,
    Queen,
    Rook,
    Bishop,
    Knight,
    Pawn
}

Notice that we derive Clone and Copy; this is so that we can put them in our board array later. You now define your symbols here. Notice how they're all in one nice place now?

#[derive(Clone, Copy)]
struct Square {
    piece: Option<Piece>
}

impl Square {
    pub fn new() -> Square {
        Square { piece: None }
    }

    fn symbol(&self) -> &str {
        match self.piece {
            Some(Piece::King)   => "K",
            Some(Piece::Queen)  => "Q",
            Some(Piece::Rook)   => "R",
            Some(Piece::Bishop) => "B",
            Some(Piece::Knight) => "k",
            Some(Piece::Pawn)   => "p",
            None                => "."
        }
    }
}

impl fmt::Display for Square {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.symbol())
    }
}

You have also eliminated the need to have any Boxes, which could have some annoying unwrapping later. It also eliminated some runtime errors and helps the compiler make sure your code is correct.

You can define your board using a 2D array like so:

pub struct Board {
    squares: [[Square; 8]; 8]
}

impl Board {
    pub fn new() -> Board {
        let mut squares = [[Square { piece: None }; 8]; 8];
        // ...
    }
}

Without seeing what you tried, I can't know why it didn't work before, it could be because you didn't derive Clone and Copy for your squares and pieces. I have opted to go for a 2D array, as I feel it is easier to reason about than a single 1D array. It also means you don't need to have lookups like myArray[x * width + y] everywhere which is just messy, I find myArray[x][y] is preferable. I won't show it, but I updated your fmt::Display for Board code to match.

Finally your board creation. Look upon the niceness of the enum, and rejoice! Also see how nice the 2D array is, and pattern matching. I really love Rust.

pub fn new() -> Board {
    let mut squares = [[Square { piece: None }; 8]; 8];

    for x in 0..8 {
        squares[x][1].piece = Some(Piece::Pawn);
        squares[x][6].piece = Some(Piece::Pawn);
        let piece = match x {
            0 | 7 => Some(Piece::Rook),
            1 | 6 => Some(Piece::Knight),
            2 | 5 => Some(Piece::Bishop),
            3 => Some(Piece::King),
            4 => Some(Piece::Queen),
            _ => unreachable!()
        };
        squares[x][0].piece = piece;
        squares[x][7].piece = piece;
    }

    Board {
        squares: squares
    }
}

It might feel as if I've really torn your code apart here, but it's not surprising given how different Rust actually is compared to what most people would call its closest competitor, C++. Implementing Clone and Copy is very easy to miss and very important if you don't want to have a nightmare of boxed instances later. Using the enum style I have here does mean that functionality for things like finding valid moves will be the job of the board, rather than the pieces, but you already started down that track. Don't forget as well that Rusts "thick" enums mean that you can still attach a moveset to your enumerated type, if that ends up being the easiest way to implement it.

\$\endgroup\$
  • \$\begingroup\$ You can write in new: squares[0] = [Some(Rook), Some(Knight), Some(Bishop), Some(King), Some(Queen), Some(Bishop), Some(Knight), Some(Rook)]; and squares[1] = [Some(Pawn); 8]; then same for squares[6] and squares[7]. \$\endgroup\$ – French Boiethios Feb 19 '18 at 12:14
  • \$\begingroup\$ True, I've also noticed that I got the rows for pawns and the rest the wrong way around. \$\endgroup\$ – Turksarama Feb 19 '18 at 13:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.