Killing bugs in Chess AI and correctly implementing alpha-beta and transpos. tables

Started by
16 comments, last by clb 18 years, 5 months ago
Quote:Original post by alvaro
Bad idea. Get your quiescence search to work as soon as possible. Everything else is secondary.

I haven't made the computations in a while, but if you store a 64-bit key and the table you use to compute the Zobrist keys is reasonably random, you will probably never see a collision.


Listen to Alvaro.

------------------http://www.nentari.com
Advertisement
On the other hand, I think I'll delay posting the sources for now. I still have a bug in my alpha-beta code but I think I can solve it on my own. Meanwhile, you can try the latest implementation at:
Chess v7 download
Chess homepage
ai snippets
ok, here goes... It is not the full source, I tried to put the most relevant parts in it. Ask me if you want to look at any headers or something else. (Please don't whine about code convention and such :) )
I found a bug in the zobrist hash creation when making a move which was probably the cause of so many table collisions. I'll fix that some time soon.

The main point is that what should I do (in which order) to make the AI stronger:
-- quiescence searches?
-- transposition tables?
-- better move evaluation? (what's the most prominent thing I should change?)

Another thing I'm worried that I think it might not mate correctly or try to draw the game if it is losing. Are there any special things I should consider when detecting draw or mate? Is it common to use a 'lazy' mate detection so that we detect mate just by a move that captures the king?
It seems that the AI prefers draw game to an obvious mate :( So that's one more bug I need to smash.

The final goal is though to make the AI 'fun' to play. I'm very poor at chess and I'd like to see the computer vary its play and try out different lines (which aren't total blunders of course) so the goal is to make it challenging. I guess this would best be achieved by first making it as strong as possible and then artificially reducing the strength.
A few comments on your code:
* You probably shouldn't use bitboards for your first chess program. The way you are evaluating and generating moves is not making good use of the bitboards, and it would be faster and more clear if you had used a format like an array of 64, a 12x10 representation, 0x88 or 16x12.
* Instead of making a copy of the board in the search function, I recommend using a single board object on which you carefully do and undo moves. For this I keep a stack of all the moves made so far together with some information that you need to undo the moves (what were the castlings allowed before the move, was an en-passant capture possible, what piece was captured...).
* I find it easier to define a few funcions (place, remove, move) that operate on the board at the lowest level, and which update the board (bitboards or array), the piece list, the zobrist key, the material count... Basically anything that you want to keep updated incrementally while you do and undo moves.
* If you want to enable the part of SearchHash that you have dissabled (dealing with hash scores that are only bounds), you need to pass alpha and beta as references or pointers, so you can modify their values. If a lower bound is found, you can use it to improve (increase) the value of alpha to that bound; similarly, if an upeer bound is found, you can use it to improve (decrease) the value of beta. If the values of alpha and beta cross, you can return the hash score from MiniMax. Oh, and this should work just find regardless of whether it's white's turn or black's turn.
* RecordHash shouldn't write to the hash if what's already there has a higher depth, because with the current code you will be forgetting important information to learn unimportant information.
* Checking the clock in every node is too expensive. If you profile your program you'll see that you spend most of your time asking for the time. Instead, check it only every few thousand nodes. You can also poll the input when you check the time, to see if you got a command that you need to deal with (like the user asking to abort the search).
* You shouldn't use std::list to store moves, because allocating and deallocating memory is expensive. Instead, use a large array and use only the part of the array that you need.
* You can use the "lazy" mate detection, but I think you are complicating things too much. What I do is set the score in MiniMax to -Infinity instead of alpha (let's look at it from white's perspective to simplify things). At the end, score will still be -Infinity if no moves were legal, and then I decide between mate and stalemate at the end. I think setting the initial value of score to alpha or beta instead of -Infinity or +Infinity is the main problem with your code. Let me post some of my code:
  int eval=-1000000000;  Move m[256]; // I am not completely sure that 256 is enough, but               // I haven't thought much about it. According to               // http://www.xs4all.nl/~timkr/records/records.htm#Most%20possible%20moves               // 75 is the maximum for positions that happened in real games.  Move *best=m;  Move *end=generate_moves(b, m); // b is a reference to the board    // Main loop of the alphabeta function  for(Move *i=m;i!=end && !abort_search;++i){    b.do_move(*i);    if(is_legal(b))      eval=-alphabeta(b,-beta,-alpha,depth-1);    b.undo_move();    if(alpha<eval){      alpha=eval;      best=i;      if(alpha>=beta)        break;    }  }    // This detects mate or stale-mate                                                                                                                 if(eval==-1000000000)    return in_check?-1000000000+b.move_number-move_number_at_root:0;

This is actual code. As you see, using NegaMax simplifies things tremendously. `b.move_number-move_number_at_root' is how far we are from the root, and that piece of code makes the program prefer shorter mates when it wins and longer mates when it loses. It also guarantees that a score of "mate" is different from -Infinity. Otherwise you would think that you are in stalemate a move leads to being mated.

I haven't checked you RootSearch function or the last thing that deals with threads (I don't think you need threads unless you are using multiple processors).

Thanks a million times, alvaro. You're being most helpful. Rating += inf.

Another thing, can you tell about statistics of how a 'decent' chess program should perform? I get about 250knodes/sec, and can search to ply 5 or 6 in about 7 seconds now (which is the AI time limit to move, iterating depths from 2 to 6)

I'm using the threads because my 3D drawing code would otherwise stall when the computer is thinking.
Quote:Original post by clb
Another thing, can you tell about statistics of how a 'decent' chess program should perform? I get about 250knodes/sec, and can search to ply 5 or 6 in about 7 seconds now (which is the AI time limit to move, iterating depths from 2 to 6)

It's very hard to compare one program to another in nodes/s or depth, because these are meaningless numbers. Well, they are not completely meaningless; if you make a change to your code that gives you a 20% improvement in nodes/s, that can only be a good thing.

The program I am working on right now (which has a mindlessly simple evaluation function), runs at about 600Knodes/s and reaches depth 10 in about 7 seconds. But of course the depth you get to depends on how aggressively you prune your tree, and what extensions you have in your search. To give you a better idea of what my "depth 10" means, running with null-move forward pruning (R=2) and extending all checks and nothing else.

Quote:I'm using the threads because my 3D drawing code would otherwise stall when the computer is thinking.

That's a very good reason to use threads. However, I would recommend running the chess engine as a separate process that knows nothing about graphics and just talks to the GUI using pipes. There are already two well-established protocols to communicate between the GUI and the engine: Winboard and UCI. I really like the design of UCI and that's what I have implemented in the program I am currenty working on. If you want to implement a GUI as well, that's fine, but Winboard or UCI compatibility will allow using your engine with other GUIs, matching your engine against other engines and playing on Internet chess clubs, like FICS and ICC.
Ok, having done some work, here are the results so far:
Chess main page
Latest build

Quote:Original post by alvaro
* You probably shouldn't use bitboards for your first chess program. The way you are evaluating and generating moves is not making good use of the bitboards, and it would be faster and more clear if you had used a format like an array of 64, a 12x10 representation, 0x88 or 16x12.


Well yes.. my move generation/evaluation isn't as effective as it could be.. I'm now thinking that might this be what's 'slowing me down' so much?

Quote:Original post by alvaro
* Instead of making a copy of the board in the search function, I recommend using a single board object on which you carefully do and undo moves.


Done, I removed all std::lists and now use only statically allocated memory, which is somewhat faster. I save the board positions in a stack and the possible moves in a queue of some sort.

Quote:Original post by alvaro
* You shouldn't use std::list to store moves, because allocating and deallocating memory is expensive. Instead, use a large array and use only the part of the array that you need.
* If you want to enable the part of SearchHash that you have dissabled (dealing with hash scores that are only bounds), you need to pass alpha and beta as references or pointers, so you can modify their values. If a lower bound is found, you can use it to improve (increase) the value of alpha to that bound; similarly, if an upeer bound is found, you can use it to improve (decrease) the value of beta. If the values of alpha and beta cross, you can return the hash score from MiniMax. Oh, and this should work just find regardless of whether it's white's turn or black's turn.
* RecordHash shouldn't write to the hash if what's already there has a higher depth, because with the current code you will be forgetting important information to learn unimportant information.
* Checking the clock in every node is too expensive. If you profile your program you'll see that you spend most of your time asking for the time. Instead, check it only every few thousand nodes. You can also poll the input when you check the time, to see if you got a command that you need to deal with (like the user asking to abort the search).


Fixed these.

Quote:Original post by alvaro
* You can use the "lazy" mate detection, but I think you are complicating things too much.


I cleaned it up a little, but I think the AI still finds checkmates in a very weird way. I previously had a bug where it preferred draws over checkmates but it should be fixed now.

Two big problems still exist:

1) The search is too slow. I'm not sure how to achieve better results, by move ordering? by some optimizations?

2) Quiescence searches take too much time because of the amount of captures can be done. Is MVV/LVA the best way to address this problem?

This topic is closed to new replies.

Advertisement