Chess: high branching factor

旧时模样 提交于 2019-12-02 16:01:26
Zong

I've developed a chess engine in C# as well, and it has a branching factor around 2.5. It is definitely possible to improve your engine by many orders of magnitudes. Nowadays the general strategy is to use very aggressive move pruning based on good move ordering. You sacrifice some correctness for the being able to see some deep tactical lines.

Here's an overview of techniques that I found to be most effective. Note that some components are complements and others are substitutes, so the results I give are general guidelines. The great gains at the end of the list are not possible if you don't have a strong foundation.

  1. Just negamax with alpha-beta pruning: depth 4 within 3 seconds.

  2. Add iterative deepening and null move heuristic: depth 5. Iterative deepening does not really help at this point, but it is easy to implement. The null move consists of skipping your turn and seeing if you can still get a beta cutoff with a shallow search. If you can, then it's probably safe to prune the tree since it's almost always advantageous to move.

  3. Killer heuristic: depth 6. This involves storing moves that cause beta cutoffs and trying them first if they are legal next time you are at the same depth. You seem to be doing something similar already.

  4. MVV/LVA ordering: depth 8. Basically, you want to put captures that have lots of potential material net gain at the top of the move list. So if a pawn captures a queen, you should obviously search it first.

  5. Bitboard representation: depth 10. This doesn't improve branching factor, but this is what I did when I reached this point. Ditch the arrays, use UInt64s instead, and use make/unmake instead of copy-make. You don't need to use magic bitboards if you find it difficult; there are simpler methods that are still very fast. Bitboards greatly improve performance and make it easy to write evaluation components. I went from perft(6) taking minutes to taking 3 seconds. (By the way, writing a perft function is a great way to ensure move generation correctness)

  6. Transposition table: depth 13. This offers great gains but is also very difficult to get right. Be absolutely certain that your position hashing is correct before implementing the table. Most of the benefit comes from the amazing move ordering the table gives you. Always store the best move into the table and whenever you get a matching position, try it first.

  7. Late move reductions: depth 16. This greatly inflates your search depth but the strength gain is more artificial than with other techniques. Basically your move ordering is so good now that you only need to fully search the first few moves in a node, and you can just check the others with shallow searches.

  8. Futility pruning: depth 17. Leaf nodes are trimmed by skipping moves that have a low chance of improving the value of the node when looking at potential material gain. If the the net potential gain of the move + static evaluation of the position is below the current value of the position, skip the evaluation for the move.

There are various other components that also help, but most are minor and some are proprietary. :D However, it is not all about high search depths and low branching factors. Things like quiescence search worsens search depth but are pretty much a necessity for any engine. Without it your engine will suffer from great tactical errors. You may also want to consider check extensions and single reply extensions. I'd also recommend at least introducing piece-square tables to your evaluation function. It's a very easy way to greatly improve the positional knowledge of your program; you'll probably see your engine playing more common openings. Chess programming is a fun hobby and I hope the volume of the information does not discourage you!

There is multiple heuristics that you can use to reduce your branching factor.

First, you should use a transposition table (TT) to store positions results, depth and best move. Before you search a move, you first check if it's already been searched at a depth >= to the depth you are planing to search to. If it is, you can simply use the result from the table. If it's not, you might still use the move in the table as your first move to search.

If there is no match in the TT for a position (inside the search), you can use Iterative Deepening (ID). Instead of doing a search to a depth of N, you first do a search to a depth of N-2. This will be really fast and will give you a move to search first at depth N.

There is also Null Move Pruning. In combination with Alpha-Beta (Negamax is a variation on Alpha-Beta) in will greatly reduce your branching factor. The idea is before searching a position, you try a null move (not playing) and do a reduce search (N-2 or N-3). The reduce search will be really fast. If the result of the null move search is still higher than beta it means the position is so bad that you don't need to search it anymore (not always true, but it is most of the time).

Of course there is multiple others heuristic you can use to improve your move ordering wich will all improve your branching factor.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!