Advertisement

Chess programming

Started by October 13, 2003 11:12 AM
30 comments, last by Druzzer007 21 years, 4 months ago
quote:
Original post by Druzzer007
well am having trouble choosing what language i can use here since i fairly know C but i really wanted to use an object oriented language so i think i will have to consider C++(which i know the basics), but my choice of java was based on the fact that later i will put it on the net and make it a network game but your ideas guys are all reasonable so if you can help me with the choice(between C and C++) i''ll be very happy.

=============================
Andrew Schwartz Moagi


If you want to put it on the net, then you should make your program a console (text mode) program and have it support the Winboard or UCI protocols. The Winboard protocol will allow your program to play on internet chess servers, and the GUI does all of the work for you. C or C++ are both good choices. There are a lot of open source programs written in C, and some in C++, and you can find a handful written in Java or VB. If you use C or C++, you''ll be able to get a lot more help, at least from my experience. Some people ask questions sometimes about "how do I do this in Delphi?" or some other less common language, and most of the time the person gets responses like, "well in C you''d do it like this...". Plus like I said there are a bunch of open source programs in C that you can look at and learn from. You really don''t need to know that much of C++ to write a chess program. A chess program should be fast, and you shouldn''t make it any more complicated than it needs to be (it will be complicated enough already), so don''t mess with polymorphism or any of the OOP stuff. If you want to make classes to ensure the validity of your data or something, that''s fine. Computer chess is a problem that has been studied for many, many decades, and there are well established ways of doing a lot of things, so using a lot of OOP stuff to arrive at the correct way of doing things really isn''t necessary. Just keep things simple.
quote:
Original post by Russell
(In response to alvaro...)

Most chess programs use one of two main board representations.

1. An 0x88-like system (link, read the section titled "A Bonus").
2. Bitboards (link).


I know. I have used them both.

quote:

If you use bitboards, you can have an array of precomputed bitboards, where each bitboard represents "the square of the pawn" that the king must be in to stop the passed pawn from promoting.
...



That is all nice and dandy, and I use it. The problem here is that this only works if there is nothing preventing the king from advancing towards the enemy pawn. In some difficult endings, especially pawn-only endings, there are situations where there are obstacles, like your own pawns, squares attacked by enemy pawns and the enemy king. It is possible to improve your static evaluation for those situations by using a path-finding algorithm that accounts for those obstacles. Whether this approach is worth the effort or not is a matter for discussion, but I have actually used it in a relatively strong program. I also penalize weak pawns for being close to the enemy king, with distance computed using the same method.

Advertisement
Ah, I see what you''re doing. A lot of people would say that''s a job for the search. I guess you and I are in the minority, because I like for my evaluation function to be as accurate as possible, so I try to evaluate these kinds of things statically as well.

If you''re using bitboards, there is are some interesting and little know algorithms that will do floodfilling and pathfinding kind of operations pretty efficiently, which might be useful for this kind of situation. The basic idea is that you do some shifting and ANDing of the bitboard to generate attacks, and you can do floodfills that "go around" squares that you specify.

As an example, you could generate attacks by floodfilling a "white rooks" bitboard in one direction, and passing in "empty squares" as the squares that are okay to fill through. Consider the following code:

typedef unsigned __int64 Bitboard;Bitboard UpAttacks (Bitboard b, Bitboard c){    b |= (b << 8) & c;    c &= (c << 8);    b |= (b << 16) & c;    c &= (c << 16);    b |= (b << 32) & c;    return b << 8;}


If you had the bitboard that represented "all white rooks", and a bitboard that represented "empty squares", you could generate attacks in the upward direction by:

Bitboard upRookAttacks = UpAttacks(whiteRooks, emptySquares);

Or you could generate rook and queen attacks by:

Bitboard upRookAttacks = UpAttacks(whiteRooks|whiteQueens, emptySquares);

You can do similar fills left, right, down, and diagonally in all directions. For just generating attacks in one direction this isn''t particularly efficient. You can combine the UpAttacks() with DownAttacks() and the other directions to write a RookAttacks() function:

Bitboard RookAttacks (Bitboard rooks, Bitboard empty){    return UpAttacks(rooks, emtpy) | DownAttacks(rooks, empty) |        LeftAttacks(rooks, empty) | RightAttacks(rooks, empty);}


Then it becomes trivial to find out which squares a rook can move to in 2 moves. Just do someting like:

Bitboard in1Move = RookAttacks(rooks, empty);
Bitboard in1Moves = RookAttacks(in1Move, empty);


Here''s some more code to play around with (below). ''b'' is usually the piece bitboard (whitePawns, blackRooks, etc.) although it could be anything that you want to generate attacks in some direction from. ''c'' is generally the squares you want to allow filling through, which is usually "empty squares". Sometimes you might want to generate a bitboard that is "safe empty squares" and you could use that to see if a king can reach a passed pawn, or whatever. This code assumes a1=0, b1=1, a2=8, etc.

// ----------// Pawn moves// ----------bitboard WhitePawnAttacks (bitboard b){return ((b << 7) & 0x7F7F7F7F7F7F7F7F) |((b << 9) & 0xFEFEFEFEFEFEFEFE);}bitboard BlackPawnAttacks (bitboard b){return ((b >> 7) & 0xFEFEFEFEFEFEFEFE) |((b >> 9) & 0x7F7F7F7F7F7F7F7F);}bitboard WhitePawnNonAttacks (bitboard b, bitboard c){b |= (b << 8) & c;c &= (c << 8);return b |= ((b & 0x0000000000FF0000) << 16) & c;}bitboard BlackPawnNonAttacks (bitboard b, bitboard c){b |= (b >> 8) & c;c &= (c >> 8);return b |= ((b & 0x0000FF0000000000) >> 16) & c;}// ------------// King Attacks// ------------bitboard KingAttacks (bitboard b){bitboard c = b;b |= (b >> 1) & 0x7F7F7F7F7F7F7F7F;b |= (b << 1) & 0xFEFEFEFEFEFEFEFE;b |= b << 8;b |= b >> 8;return b ^ c;}// --------------// Knight Attacks// --------------bitboard KnightAttacks (bitboard b){bitboard c = b;b ^= c;b |= (c >> 17) & 0x7F7F7F7F7F7F7F7F;b |= (c << 15) & 0x7F7F7F7F7F7F7F7F;b |= (c >> 15) & 0xFEFEFEFEFEFEFEFE;b |= (c << 17) & 0xFEFEFEFEFEFEFEFE;b |= (c >> 10) & 0x3F3F3F3F3F3F3F3F;b |= (c << 6) & 0x3F3F3F3F3F3F3F3F;b |= (c >> 6) & 0xFCFCFCFCFCFCFCFC;b |= (c << 10) & 0xFCFCFCFCFCFCFCFC;return b;}// ---------------------// Rank and file attacks// ---------------------bitboard UpAttacks (bitboard b, bitboard c){b |= (b << 8) & c;c &= (c << 8);b |= (b << 16) & c;c &= (c << 16);b |= (b << 32) & c;return b << 8;}bitboard DownAttacks (bitboard b, bitboard c){b |= (b >> 8) & c;c &= (c >> 8);b |= (b >> 16) & c;c &= (c >> 16);b |= (b >> 32) & c;return b >> 8;}bitboard LeftAttacks (bitboard b, bitboard c){c &= 0x7F7F7F7F7F7F7F7F;b |= (b >> 1) & c;c &= (c >> 1);b |= (b >> 2) & c;c &= (c >> 2);b |= (b >> 4) & c;c &= (c >> 4);return (b >> 1) & 0x7F7F7F7F7F7F7F7F;}bitboard RightAttacks (bitboard b, bitboard c){c &= 0xFEFEFEFEFEFEFEFE;b |= (b << 1) & c;c &= (c << 1);b |= (b << 2) & c;c &= (c << 2);b |= (b << 4) & c;c &= (c << 4);return (b << 1) & 0xFEFEFEFEFEFEFEFE;}bitboard RankFileAttacks (bitboard b, bitboard c){return UpAttacks(b,c) |DownAttacks(b,c) |LeftAttacks(b,c) |RightAttacks(b,c);}// ----------------// Diagonal attacks// ----------------bitboard DownLeftAttacks (bitboard b, bitboard c){c &= 0x7F7F7F7F7F7F7F7F;b |= (b >> 9) & c;c &= (c >> 9);b |= (b >> 18) & c;c &= (c >> 18);b |= (b >> 27) & c;c &= (c >> 27);return (b >> 9) & 0x7F7F7F7F7F7F7F7F;}bitboard UpLeftAttacks (bitboard b, bitboard c){c &= 0x7F7F7F7F7F7F7F7F;b |= (b << 7) & c;c &= (c << 7);b |= (b << 14) & c;c &= (c << 14);b |= (b << 21) & c;c &= (c << 21);return (b << 7) & 0x7F7F7F7F7F7F7F7F;}bitboard DownRightAttacks (bitboard b, bitboard c){c &= 0xFEFEFEFEFEFEFEFE;b |= (b >> 7) & c;c &= (c >> 7);b |= (b >> 14) & c;c &= (c >> 14);b |= (b >> 21) & c;c &= (c >> 21);return (b >> 7) & 0xFEFEFEFEFEFEFEFE;}bitboard UpRightAttacks (bitboard b, bitboard c){c &= 0xFEFEFEFEFEFEFEFE;b |= (b << 9) & c;c &= (c << 9);b |= (b << 18) & c;c &= (c << 18);b |= (b << 27) & c;c &= (c << 27);return (b << 9) & 0xFEFEFEFEFEFEFEFE;}bitboard DiagonalAttacks (bitboard b, bitboard c){return DownLeftAttacks(b,c) |UpLeftAttacks(b,c) |DownRightAttacks(b,c)|UpRightAttacks(b,c);}
So as you guys have mentioned how does this 88-like system handle the middle game tactics,as to my understanding, the branching factor here is a little bit higher and does this not have significant effect on the perfomance of the program. And generally what are the ratings(FIDE) the programs done using this system generally accumulate.

=============================
Andrew Schwartz Moagi

[edited by - Druzzer007 on October 14, 2003 5:52:13 PM]

[edited by - Druzzer007 on October 14, 2003 5:53:55 PM]
=============================Andrew Schwartz Moagi
quote:
Original post by Russell
typedef unsigned __int64 Bitboard;Bitboard UpAttacks (Bitboard b, Bitboard c){    b |= (b << 8) & c;    c &= (c << 8);    b |= (b << 16) & c;    c &= (c << 16);    b |= (b << 32) & c;    return b << 8;}





That is the coolest thing ever. In exchange, I'll share with you a little trick that I think might be useful. I don't like writing every function twice, once for white and once for black. This past weekend I came up with the following neat C++ solution:
      enum Color{White,Black};template <Color c>struct ColorTraits;template <>struct ColorTraits<White>{   static const Color Enemy = Black;   static u64 forward(u64 x){ return x<<8; }   static u64 backwards(u64 x){ return x>>8; }   static const Piece Pawn = WhitePawn;   static const Piece Night = WhiteNight;   //...};             template <>struct ColorTraits<Black>{   static const Color Enemy = White;   static u64 forward(u64 x){ return x>>8; }   static u64 back(u64 x){ return x<<8; }   static const Piece Pawn = BlackPawn;   static const Piece Knight = BlackKnight;   //...};template <Color c>u64 passed_pawns(){                   u64 obstacles = bitboard[ColorTraits<ColorTraits<c>::Enemy>::Pawn];   obstacles = ColorTraits<c>::back(obstacles);                       obstacles |= Left(obstacles) | Right(obstacles); // Left and Right have obvious definitions   obstacles |= ColorTraits<c>::back(obstacles);   obstacles |=    ColorTraits<c>::back(    ColorTraits<c>::back(obstacles));   obstacles |=    ColorTraits<c>::back(    ColorTraits<c>::back(                    ColorTraits<c>::back(    ColorTraits<c>::back(obstacles))));   return bitboard[ColorTraits<c>::Pawn] & ~obstacles;}

I think the ColorTraits template is pretty neat. I also discovered that my compiler (g++-3.2) does not optimize the four concatenated calls to `back' properly, so I provided a function `back4' that rotates 32 places. Other than that, the assembly code looks very good.



[edited by - Alvaro on October 14, 2003 6:13:05 PM]
quote:
Original post by Druzzer007
So as you guys have mentioned how does this 88-like system handle the middle game tactics,as to my understanding, the branching factor here is a little bit higher and does this not have significant effect on the perfomance of the program. And generally what are the ratings(FIDE) the programs done using this system generally accumulate.


You can write a good program using any board representation. Different board representations give you different things for "free" (meaning, they do some things very quickly). They each have their own advantages. Pick the one that makes the most sense to you and learn all about the advantages of that system, and you'll be fine.

I have discovered over time that pretty much anything I can do in one board representation, I can do in another just as efficiently. The hard part is learning how to use the tools that each system provides to acheive what you want to achieve. For instance, thinking in terms of bitboards is quite different from using an 0x88 system.

IMO, the board representation that will get you the most bang for your buck is a 12x16 board. This allows you to use the 0x88 trick, and scanning the board is more efficient. When you scan an 0x88 board (8x16), you typically do something like this:

1. Is this square on the board (uses the 0x88 trick)
2. Is this square empty
3. If it's empty, generate a move (or whatever), else stop scanning.

Using a 12x16 board, you can still use the 0x88 trick, but it really isn't necessary. Now you can just do:

1. Is the square empty?
2. If it's empty, generate a move (or whatever), else stop scanning.

If a square is off the board, it won't be "empty" (it will be "invalid" or whatever you want to set it to). It's not a huge difference, but it's better if you do a lot of scanning of the board. The 8x8 board is in the middle of the 12x16 board. It will look like this (X means "off the board"):


XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
--------XXXXXXXX
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXX


I use bitboards however, because I think they offer a little more once you really learn how to use them. If you're starting out though, use something simple. You'll have plenty of other stuff to learn. It seems like you're picking up on things pretty fast though, so you should do well.

[edited by - Russell on October 14, 2003 6:33:43 PM]
Advertisement
quote:
Original post by alvaro
That is the coolest thing ever.
I thought so too when I first saw it I'll warn you though, that it's not the fastest code on 32-bit hardware for doing simple attacks. Rotated bitboards are way faster (unless you can make it work using MMX or something, which I know one person did). You really need to do something like floodfilling where you can fill the entire board in a handful of iterations (as opposed to doing recursive scanning all over the board). It also might be worth it if you want to know something like "how many squares can this rook move to in N moves?".

quote:

In exchange, I'll share with you a little trick that I think might be useful. I don't like writing every function twice, once for white and once for black. This past weekend I came up with the following neat C++ solution:
         enum Color{White,Black};template <Color c>struct ColorTraits;template <>struct ColorTraits<White>{   static const Color Enemy = Black;   static u64 forward(u64 x){ return x<<8; }   static u64 backwards(u64 x){ return x>>8; }   static const Piece Pawn = WhitePawn;   static const Piece Night = WhiteNight;   //...};             template <>struct ColorTraits<Black>{   static const Color Enemy = White;   static u64 forward(u64 x){ return x>>8; }   static u64 back(u64 x){ return x<<8; }   static const Piece Pawn = BlackPawn;   static const Piece Knight = BlackKnight;   //...};template <Color c>u64 passed_pawns(){                   u64 obstacles = bitboard[ColorTraits<ColorTraits<c>::Enemy>::Pawn];   obstacles = ColorTraits<c>::back(obstacles);                       obstacles |= Left(obstacles) | Right(obstacles); // Left and Right have obvious definitions   obstacles |= ColorTraits<c>::back(obstacles);   obstacles |=    ColorTraits<c>::back(    ColorTraits<c>::back(obstacles));   obstacles |=    ColorTraits<c>::back(    ColorTraits<c>::back(                    ColorTraits<c>::back(    ColorTraits<c>::back(obstacles))));   return bitboard[ColorTraits<c>::Pawn] & ~obstacles;}

I think the ColorTraits template is pretty neat. I also discovered that my compiler (g++-3.2) does not optimize the four concatenated calls to `back' properly, so I provided a function `back4' that rotates 32 places. Other than that, the assembly code looks very good.

Very cool and elegant! I'll play with it when I get home from work.



[edited by - Russell on October 14, 2003 6:40:26 PM]
Would I be correct in assuming that bitboards will be used in the overwhelming majority of implementations once 64-bit cpu''s (ie. AMD x86-64) become commonplace? It would seem to me that bitboards will receive a HUGE boost in performance once we get 64-bit cpu''s.
quote:
Original post by Optikal
Would I be correct in assuming that bitboards will be used in the overwhelming majority of implementations once 64-bit cpu''s (ie. AMD x86-64) become commonplace? It would seem to me that bitboards will receive a HUGE boost in performance once we get 64-bit cpu''s.
It depends upon what kind of program we''re talking about. I doubt you will see many commercial engines go to this in the near future, because their goal is to make money, and most people still have 32-bit machines. It will take a while before 64-bit machines are the majority. Already there are a lot of amateur programs that are using bitboards.

On 32-bit hardware, bitboards tend to "break even" with other approaches. The hope is that they will receive a bigger speed boost on 64-bit hardware, but some people have reported that their non-bitboard programs are almost twice as fast on an Opteron when compared to an Athlon at the same GHz. Common sense would tell us that a bitboard program would benefit even more.

It''s kind of up in the air until the 64-bit stuff becomes more mainstream, the price comes down, and they get all of the other stuff in order (operating systems, compilers, etc.).
quote:
ColorTraits


Just out of curiosity (I''m not that experienced with the details of templates), how fast would your ColorTraits approach perform compared to other approaches? And what does it convert to? Does it do an if statement under the hood or a table lookup or a function pointer, or what? Like I said, I''m not all that familiar with how the compiler translates these kind of templated things.

This topic is closed to new replies.

Advertisement