nkarve / surge

A fast bitboard-based chess move generator in C++
MIT License
63 stars 15 forks source link

How do you switch sides and why is side not detected? #23

Closed archishou closed 1 year ago

archishou commented 1 year ago

Hey, I'm trying to write something I think should be pretty simple. Play a random move for each side until checkmate.

int main() {
    initialise_all_databases();
    zobrist::initialise_zobrist_keys();
    Position p;
    Position::set("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -", p);
    std::cout << p;

    int moveListLength = 1;

    while (moveListLength != 0) {
        MoveList<WHITE> moveList(p);
        std::random_device dev;
        std::mt19937 rng(dev());
        std::uniform_int_distribution<std::mt19937::result_type> dist6(0,moveListLength);
        Move randMove = moveList.list[dist6(rng)];
        p.play<WHITE>(randMove);
        moveListLength = sizeof(moveList.list) / sizeof(moveList.list[0]);

        std::cout << randMove << std::endl;
        std::cout << p << std::endl;
    }

    return 0;
}

This is what I've got going right now, but obviously it doesn't switch sides after white plays. My questions are this

  1. How would I make something so that the sides switch, I tried the ~Us idea shown in chess-engine.cpp file but that only works because its recursive, in this iterative approach, I can't see how to do something similar because the Color must be a constant. So I can't do something simple like Color team = White and then do something like team = ~team at the end of the loop.
  2. Why isn't the color just detected from the position, it seems like the position class stores a side_to_play variable and since both moveList and p.play use a position, why not just use the side_to_play variable inside each position? Seems redundant and a little confusing for that not to be the case.

I'm extremely new to C++ (Just a day in!) So if my questions are obvious or trivial sorry in advance!

archishou commented 1 year ago

Ok, I was able to solve it with this (manually switching between two move lists), but I'm now getting a seg fault. Wondering if this may be an issue with the library itself or something I'm doing.

template <Color color>
Move randomMove(Position board) {
    MoveList<color> moveList(board);
    std::vector<Move> out;
    size_t nelems = 1;
    std::sample(
            moveList.begin(),
            moveList.end(),
            std::back_inserter(out),
            nelems,
            std::mt19937{std::random_device{}()}
    );
    return out[0];
}
template <Color color>
int checkmate(Position board) {
    std::cout << "here 2" << std::endl;
    MoveList<color> moveList(board);
    std::cout << "here 3" << std::endl;
    return moveList.size() == 0;
}

int main() {
    initialise_all_databases();
    zobrist::initialise_zobrist_keys();
    const std::string& startFen ="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" ;
    Position board;
    Position::set(startFen, board);
    std::cout << board;

    int totalMovesMade = 0;
    while (!checkmate<WHITE>(board) && !checkmate<BLACK>(board)) {
        std::cout << "here 0" << std::endl;
        if (!checkmate<WHITE>(board)) {
            std::cout << "here 4" << std::endl;
            Move move = randomMove<WHITE>(board);
            std::cout << "here 5" << std::endl;
            board.play<WHITE>(move);
            totalMovesMade += 1;
            std::cout << board << totalMovesMade << std::endl;
        }
        std::cout << "here 1" << std::endl;
        if (!checkmate<BLACK>(board)) {
            std::cout << "here 6" << std::endl;
            Move move = randomMove<BLACK>(board);
            std::cout << "here 7" << std::endl;
            board.play<BLACK>(move);
            totalMovesMade += 1;

            std::cout << board << totalMovesMade << std::endl;
        }
        std::cout << "here 10" << std::endl;
    }

    return 0;
}
nkarve commented 1 year ago

Sure, you could do something like

// Color Us has the first move in the position

template<Color Us>
void play_random_game(Position& p) {
    std::random_device dev;
    std::mt19937 rng(dev());

    int max_length = 100;

    for(int i = 0; i < max_length; i++) {
        MoveList<Us> our_list(p);
        if(our_list.size()) {
            std::uniform_int_distribution<std::mt19937::result_type> dist(0, our_list.size() - 1);
            int idx = dist(rng);
            Move move = *(our_list.begin() + idx);
            p.play<Us>(move);

            std::cout << move << "\n";
        } else break;

        MoveList<~Us> their_list(p);
        if(their_list.size()) {
            std::uniform_int_distribution<std::mt19937::result_type> dist(0, their_list.size() - 1);
            int idx = dist(rng);
            Move move = *(their_list.begin() + idx);
            p.play<~Us>(move);

            std::cout << move << "\n";
        } else break;
    }

    std::cout << p;
}

The issue with playing until the move list is empty is that the program does not currently support the 50 move rule. So technically you could have king vs king ad infinitum.

nkarve commented 1 year ago

Also the reason automatic side detection wasn't supported out of the box is because there are advanced move generation and eval techniques that involve playing moves "out of turn"

archishou commented 1 year ago

Hm I've run your code, and If I set the maxmimum length to anything around 300, I get a seg fault. Do you know why that might be?

archishou commented 1 year ago

Also the reason automatic side detection wasn't supported out of the box is because there are advanced move generation and eval techniques that involve playing moves "out of turn"

Also, thats pretty cool, I definitely take a look at that.

nkarve commented 1 year ago

The segfault almost definitely comes from UndoInfo history[256] in position.h overflowing - increase that to 1000 or something and it should be fine

archishou commented 1 year ago

Ah! That makes a lot of sense. My debugger kept pointing to something in the pawn attacks and left me very confused. Thanks for helping me out!

archishou commented 1 year ago

Ok... final question, I promise. I seem to have come across a bug in the .play() method. See this reproducible example below.

int main() {
    initialise_all_databases();
    zobrist::initialise_zobrist_keys();

    const std::string& startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
    Position p;
    Position::set(startFen, p);
    const std::string& Line = "c2c4 h7h5 d2d4 h8h7 c1f4 c7c5 d4c5 g7g6 g1h3 d8a5 b1c3 a5c3 d1d2 c3c1";
    for (int i = 0; i < Line.size(); i += 5) {
        Move nextMove(Line.substr(i, 4));
        if (p.turn() == BLACK) {
            std::cout << MoveList<BLACK>(p).size() << std::endl;
            p.play<BLACK>(nextMove);
        }
        else {
            std::cout << MoveList<WHITE>(p).size() << std::endl;
            p.play<WHITE>(nextMove);
        }
    }
    if (p.turn() == BLACK) {
        std::cout << MoveList<BLACK>(p).size() << std::endl;
    }
    else {
        std::cout << MoveList<WHITE>(p).size() << std::endl;
    }
    Position check;
    Position::set(p.fen(), check);
    if (check.turn() == BLACK) {
        std::cout << MoveList<BLACK>(check).size() << std::endl;
    }
    else {
        std::cout << MoveList<WHITE>(check).size() << std::endl;
    }
    return 0;
}

The position check has 3 possible total moves (which is correct according to stock fish). The position p has one more, 4. However, both have the same FEN.

Is this a mistake on how I'm using the library or have I truly come across a mistake in the code?

My use case here is parsing UCI input. I want to start from the default board position, grab the UCI moves in the order the appear, make those moves on the board, and then have my engine use this new board to find the best position. I'm looking at the issues and it seems my issue may be coming from #11 . If that is the case, I'm wondering if there is some alternative I'm not aware of.

UCI input usually looks something like: position startpos moves a2a3 g8h6 g1h3 e7e5 h1g1 d8e7 c2c3 a7a5 h3g5 e7c5 g2g3 e5e4 b2b4 c5e3 g1h1 a8a6 h1g1 b7b5 c3c4 f7f5 d2d4 e3a3 a1a2 a6a7 f2f3 c7c6 c1f4 a7a6 d1c2 a3b3 c2e4 e8d8 which is why I formated the testcase above similarly, not sure how else to go about this.

nkarve commented 1 year ago

You need to make sure all capture moves have the CAPTURES flag enabled, otherwise it leads to bugs (because of that you can't directly parse UCI, as you have found out, and as the other issue mentions)