rudzen / ChessLib

C# chess library containing a complete data structure and move generation.
MIT License
81 stars 23 forks source link

Some questions #62

Open SloPro opened 1 year ago

SloPro commented 1 year ago

Hello, I just have a few questions related mainly to making moves.

What is intended to be passed to position.MakeMove as the in State parameter? I've just been using it like game.Pos.MakeMove(move, new State());, since based on my digging into the source code and its provided tests, this seemed about alright, but I'm just wondering if that's how you're meant to do it.

Secondly, I've been having some issues with trying to implement my own simple transposition table in my Negamax search (yes I know this comes with a prepared TT implementation already).

Namely, the Position.Key Zobrist hash seems to be just not working well at all in my Negamax search (implementation is literally from the wikipedia pseudocode), after some preliminary debugging there seem to just be lots of collisions happening, which completely breaks the search results.

As a test, I even tried reusing the same code but using the current position's FEN (pos.GenerateFen().ToString()) as the table key instead, and while it was obviously much slower, it did end up searching fine and working as expected, indicating it's just the Zobrist keys that are the issue:

//base search (without any transposition table)
ply 1 after 0.06 s (60 ms), best move: Bxg5+ [-3.60] nodes: 2,686 (44,767 n/s), ABcuts 0, TTskips: 0, TTentries: 0
ply 2 after 0.24 s (177 ms), best move: Bxg5+ [9999.97] nodes: 12,795 (72,288 n/s), ABcuts 479, TTskips: 0, TTentries: 0
ply 3 after 0.60 s (359 ms), best move: Bxg5+ [9999.97] nodes: 59,177 (164,838 n/s), ABcuts 1,546, TTskips: 0, TTentries: 0
ply 4 after 1.53 s (919 ms), best move: Bxg5+ [9999.97] nodes: 304,543 (331,385 n/s), ABcuts 19,111, TTskips: 0, TTentries: 0
ply 5 after 4.49 s (2959 ms), best move: Bxg5+ [9999.97] nodes: 1,431,047 (483,625 n/s), ABcuts 52,850, TTskips: 0, TTentries: 0

// using TT with FEN as the key
ply 1 after 0.06 s (55 ms), best move: Bxg5+ [-3.60] nodes: 2,686 (48,836 n/s), ABcuts 0, TTskips: 0, TTentries: 22
ply 2 after 0.28 s (216 ms), best move: Bxg5+ [9999.97] nodes: 12,795 (59,236 n/s), ABcuts 479, TTskips: 0, TTentries: 564
ply 3 after 0.62 s (348 ms), best move: Bxg5+ [9999.97] nodes: 56,922 (163,569 n/s), ABcuts 1,436, TTskips: 141, TTentries: 2,307
ply 4 after 1.53 s (902 ms), best move: Bxg5+ [9999.97] nodes: 276,624 (306,678 n/s), ABcuts 16,424, TTskips: 2,409, TTentries: 17,803
ply 5 after 4.36 s (2829 ms), best move: Bxg5+ [9999.97] nodes: 1,126,297 (398,125 n/s), ABcuts 42,200, TTskips: 8,523, TTentries: 56,734

// using TT with game.Pos.State.Key.Key as the key
ply 1 after 0.08 s (83 ms), best move: Bxg5+ [-3.60] nodes: 2,686 (32,361 n/s), ABcuts 0, TTskips: 0, TTentries: 22
ply 2 after 0.25 s (163 ms), best move: Bxg5+ [9999.97] nodes: 5,906 (36,233 n/s), ABcuts 258, TTskips: 269, TTentries: 307
ply 3 after 0.40 s (137 ms), best move: Rg6+ [9999.97] nodes: 12,054 (87,985 n/s), ABcuts 680, TTskips: 1,747, TTentries: 800
ply 4 after 0.59 s (185 ms), best move: Rg6+ [9999.97] nodes: 17,288 (93,449 n/s), ABcuts 1,391, TTskips: 3,074, TTentries: 1,529
ply 5 after 0.69 s (103 ms), best move: Rg6+ [9999.97] nodes: 24,949 (242,223 n/s), ABcuts 2,312, TTskips: 5,268, TTentries: 2,641
ply 6 after 0.94 s (244 ms), best move: Rg6+ [9999.97] nodes: 91,503 (375,012 n/s), ABcuts 8,394, TTskips: 20,383, TTentries: 8,986
ply 7 after 1.41 s (470 ms), best move: Rg6+ [9999.97] nodes: 217,112 (461,940 n/s), ABcuts 19,105, TTskips: 55,142, TTentries: 20,676
ply 8 after 1.85 s (437 ms), best move: Rg6+ [9999.97] nodes: 205,691 (470,689 n/s), ABcuts 19,883, TTskips: 52,802, TTentries: 21,700
ply 9 after 3.18 s (1325 ms), best move: Rg6+ [9999.97] nodes: 606,702 (457,888 n/s), ABcuts 61,909, TTskips: 188,832, TTentries: 64,696

Could this perhaps be related to me just passing a new State() every time I make a move in the search? e.g.:

foreach (var move in game.Pos.GenerateMoves())
{
      game.Pos.MakeMove(move, new State());
      int n = -Negamax(ref game, depth - 1, -beta, -alpha, -color);
      game.Pos.TakeMove(move);
      ...

Thanks in advance for your time!

rudzen commented 1 year ago

Yes you can pass in a new State() object, it would still work since it keeps reference internally. For more controlled use of the states you use, you would have to keep that of those yourself, f.x. through a data structure you devise yourself or using a stack etc.

Yes, I'm aware there are some problems with the current Zobrist implementation, it "works" but causes me some problems with my own engine implementation. For now what you use as a replacement would technically work - but yeah it's kind of slow.

I'm currently attempting to figure out a clever way of fixing it.

Thanks for the details 👍🏻