jordanbray / chess

A rust library to manage chess move generation
https://jordanbray.github.io/chess/
MIT License
234 stars 54 forks source link

Usability questions and proposals #31

Closed ikamensh closed 3 years ago

ikamensh commented 4 years ago

Hey there, thanks for creating and maintaining this crate. I picked it to experiment with classical AI algorithms and practice rust. I have faced some usability issues, which might be in part due to me not understanding how best to handle it:

1) Iterating over pieces: Board::pieces() method is what I would expect to give an iterator over all pieces present on the board. I want to implement a heuristic value of the board based on sum of weighted pieces for each side. This method currently gives BitBoard, and I have no idea how to use it. Would you be interested to add a method Board::pieces_iter( &self ) -> Iterator<(Piece, Color, Square)> ?

My current workaround is to map from characters in Board::to_string() to (Piece, Color, Square), you can guess it is kind of ugly :)

2) Not sure if it is sensible to expect this, but MoveGen does not implement choose to get a random turn. Again, my purpose of practicing rust can be served by adding this if you would merge such PR.

jordanbray commented 4 years ago

Well, there's a few questions built in here.

  1. It sounds like you want a method based on the sum of the weights of each piece. The best way to do this is something like:
let black_pawns = (board.pieces(Piece::Pawn) & board.color_combined(Color::Black)).popcnt();
let white_pawns = (board.pieces(Piece::Pawn) & board.color_combined(Color::White)).popcnt();
...
return (white_pawns - black_pawns) * 100 + (white_knights - black_knights) * 300 ...

This saves a lot of time by not using an iterator and allowing bitwise intrinsics of the CPU here.

This is also why there isn't an iterator like the one you want. You'd end up writing code much slower than using the bitboards directly. (That popcnt, for example? One instruction with target-cpu=native. The and? One instruction.).

  1. Keep in mind every BitBoard implements Iterator<Square>. So, once you have the white_pawns, you can easily do a for loop over the squares in the BitBoard.

  2. I could be convinced that a function like this could exist:

    impl Board {
    ...
    pub fn pieces_of(&self, piece: Piece, color: Color) -> BitBoard {
       self.pieces(piece) & self.color_combined(color)
    }
    }

    to shorten the boilerplate.

  3. I just don't like the iterator. as designed above. I would rather push people towards the bitwise operators as much as possible, because they really do boil down to just a few (sometimes one) instructions.

  4. So, MoveGen does implement iterator, which means I'm sure one of the random crates would handle a choice function. It even implements ExactSizeIterator, which should make it easy to implement in a more general sense somewhere else. I wouldn't want it to exist in my crate, because random numbers are a hard problem, and really belong in a dedicated crate.

ikamensh commented 4 years ago

On pieces_iter: reasoning is following:

1) Getting a great algorithmic complexity beats constant factors (iterator over pieces would be slower than bitwise operations by a constant factor) 2) life is short, should every student / researcher / hobbyist playing with this library re-implement non-trivial bitwise operations? It really requires understanding implementation details of this crate. As a user, I want to delay learning implementation details of my libraries until I really need it. 3) people who care about performance will surely pick up on BitBoard implementation and do their thing. 4) Chess is not a business problem. It is far more likely to be an educational problem (trying to understand typical user profile)

By going for Rust vs e.g. Python I am already so much ahead on performance, I would like to be able to first build a prototype of what I want, and optimize later.

I really respect that you have limited scope for this crate. Representing board appears to be in this scope. One can't have the most performant implementation for unknown use-case. I think this justifies offering generic, clear and correct API as reference that is not most performant.

At the same time, we could put your example of performant calculation of value into examples section as a hint to those more caring about performance.

ikamensh commented 4 years ago

p.s. what can happen in the absence of standard implementation is that people might implement something which is yet worse than the iterator. Here is what I have built before your message:

fn evaluate(b: & Board, color: Color) -> f64 {
    let mut sum_white = 0.0;
    let mut sum_black = 0.0;

    for c in b.to_string().chars(){
        if !c.is_ascii_digit(){
            let (piece, color) = CHARS_TO_PIECES[c];
            match color{
                Color::White => { sum_white += PIECES_VALUE[piece]; }
                Color::Black => { sum_black += PIECES_VALUE[piece]; }
            }
        }
    }
    ...
}
jordanbray commented 4 years ago

I'm sorry to say I'm still not convinced. In my opinion, you're going to have a much better time writing code if you stick to bitboards. Yes, there is a bit of mental overhead when first starting out, but it really becomes quite intuitive in a very short amount of time.

If you must use a for loop over color, piece, square, you could use something like the following:

for p in ALL_PIECES {
    for c in ALL_COLORS {
        for sq in (board.pieces(p) & board.color_combined(c)) {
            // you now have, p, c, and sq in scope
        }
    }
}

You could probably chain those iterators together quite easily as well.

That said, there are a lot of algorithms with regards to evaluation, pruning, transposition tables, pawn structure (!), that pretty much require you to use bitboards directly. Even heuristics like butterfly boards require their use. Bitboards are such a fundamental data structure in chess programming, that you will eventually have to learn them, not to optimize your code, but simply to read existing literature.

I see your point that I'm exposing the underpinnings of my library, and you don't want to deal with that, but I don't feel that's accurate. I feel like I'm exposing a data structure that maps to existing research in chess programming, and reducing boilerplate when following such literature.

As for people writing bad code, there's no amount of libraries or API design that will prevent that.

jordanbray commented 4 years ago

Another reasonable approach may be to add more documentation related to BitBoards. If i'm going to hang my hat on those as the data-structure of choice, I think the main page of the documentation could explain that and give some operations / identities to get people started.

Something like "white_pieces contain all the white pieces, rooks contains all the rooks, white_pieces & rooks contain all the white rooks.", and a few more similar examples.