SEE algorithm on chessprogramming wiki

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

mcostalba
Posts: 2684
Joined: Sat Jun 14, 2008 9:17 pm

Re: SEE algorithm on chessprogramming wiki

Post by mcostalba »

bob wrote: This has very low cost.
Yes, I agree.

One interesting problem is to see if there is some benefit in adding the burden to detect captures by pinned pieces.

I have experimented a bit some months ago with this, but I have no found any measurable gain in real games, so I dropped also this one :-(
mcostalba
Posts: 2684
Joined: Sat Jun 14, 2008 9:17 pm

Re: SEE algorithm on chessprogramming wiki

Post by mcostalba »

mcostalba wrote:
Sven Schüle wrote: What do you think is the portion of captures longer than 2 pieces?
I am not able to measure now this parameter. I will do later this evening and post the result.
1) In about 20% of cases opponent cannot recapture root move

2) In the remaining 80% of cases this is the statistical distribution

- 70% chain length 1 (opponent recaptures and we don't have any piece to go on)

- 17% chain length 2

- 11% chain length 3

- 2% chain length above 3


So, after merging results at points (1) and (2) we have:

Code: Select all

20%   PXN 

56%   PXN, BXP

13%   PXN, BXP, RXB

9%    PXN, BXP, RXB, QXR

2%    remaining longer chains
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: SEE algorithm on chessprogramming wiki

Post by bob »

mcostalba wrote:
bob wrote: This has very low cost.
Yes, I agree.

One interesting problem is to see if there is some benefit in adding the burden to detect captures by pinned pieces.

I have experimented a bit some months ago with this, but I have no found any measurable gain in real games, so I dropped also this one :-(
I tried this a year or two ago when it first came up. I first ran a bunch of fixed-depth games, and modified SEE to just blindly look for pinned pieces and factor in the cost of moving that piece in addition to how it contributes to the capture. I didn't try to do this efficiently because I was not sure the idea was worth anything. Using fixed depth, I found no difference in old and new. But for my implementation, new was certainly expensive. Net result would have been horrible if I had tried using time. But with no measurable benefit, I simply dropped the idea. SEE can be wrong for other reasons just as frequently, including using overloaded pieces, or moving a piece that is not "directly pinned" but which is blocking a key attack on a square that is important. I think there's enough errors that eliminating just pinned pieces is not going to help enough to make it worthwhile, even if it can be done very fast.
User avatar
michiguel
Posts: 6401
Joined: Thu Mar 09, 2006 8:30 pm
Location: Chicago, Illinois, USA

Re: SEE algorithm on chessprogramming wiki

Post by michiguel »

bob wrote:
mcostalba wrote:
bob wrote: This has very low cost.
Yes, I agree.

One interesting problem is to see if there is some benefit in adding the burden to detect captures by pinned pieces.

I have experimented a bit some months ago with this, but I have no found any measurable gain in real games, so I dropped also this one :-(
I tried this a year or two ago when it first came up. I first ran a bunch of fixed-depth games, and modified SEE to just blindly look for pinned pieces and factor in the cost of moving that piece in addition to how it contributes to the capture. I didn't try to do this efficiently because I was not sure the idea was worth anything. Using fixed depth, I found no difference in old and new. But for my implementation, new was certainly expensive. Net result would have been horrible if I had tried using time. But with no measurable benefit, I simply dropped the idea. SEE can be wrong for other reasons just as frequently, including using overloaded pieces, or moving a piece that is not "directly pinned" but which is blocking a key attack on a square that is important. I think there's enough errors that eliminating just pinned pieces is not going to help enough to make it worthwhile, even if it can be done very fast.
To the moderators: Don Dailey has hijacked Bob's account :-)

Miguel
User avatar
Daniel Mehrmann
Posts: 858
Joined: Wed Mar 08, 2006 9:24 pm
Location: Germany
Full name: Daniel Mehrmann

Re: SEE algorithm on chessprogramming wiki

Post by Daniel Mehrmann »

Hello Gerd,

i'm using different kind of SEE stuff:

LazySEE
Only says if a capture is a winner or not. It's more or less based on the same idea like your chesswiki suggest. I call it "LazySEE". It's faster as a (full)SEE.

(Full)SEE
can says if its a winner, equal or a looser (In all cases the exact value). It calculates all material possibilities. It's slower, but giving back a excat score always.

Just an example from your side:

[D]1k1r3q/1ppn3p/p4b2/4p3/8/P2N2P1/1PP1R1BP/2K1Q3 w - -

Starting move: Nxe5

LazySEE gives back 0
(Full)SEE gives back -320, because we lose a Night, but win a pawn.

I'm using my (Full)SEE for move order to determine the captures.... ;-)

Best,
Daniel
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: SEE algorithm on chessprogramming wiki

Post by hgm »

I don't think this issue can be fully clear without exactly seeing what is involved in the itertive version. So let me have a stab at this:

This is the regular alpha-beta search as applied to SEE, where there is no loop over moves, as at any stage we only try stand pat or the capture to the given target with the lowest attacker:

Code: Select all

int SEE(int apha, int beta, int eval)
{
  if(eval >= beta) return beta; // stand pat produces (fail-hard) cutoff
  if(eval > alpha) alpha = eval; // this is the crucial alpha update
  // FOR ALL MOVES
  MakeNextCapture();
  score = -SEE(-beta, -alpha, -(eval + PieceValue[victim]));
  UnMake();
  if(score > alpha) alpha = score;
  // NEXT MOVE
  return alpha;
}
Note that with fail-hard, the returned score will be at least alpha, so the update of alpha can be made unconditionally, and the code becomes:

Code: Select all

int SEE(int apha, int beta, int eval)
{
  if(eval >= beta) return beta; // stand pat produces (fail-hard) cutoff
  if(eval > alpha) alpha = eval; // this is the crucial alpha update
  MakeNextCapture();
  alpha = -SEE(-beta, -alpha, -(eval + PieceValue[victim]));
  UnMake();
  return alpha;
}
Now note that this is a 'tail recursion' except for the UnMake(). So after we reach the deepest level, the whole thing unwinds by doing a series of UnMakes. Now the representation we use during a SEE is usually not a board, but some sorted lists of captures to the given square, made at the beginning. And we do not want to use these lists again; after we are done wth the SEE, we will just discard them. So there really is no reason to UnMake() anything at all, and UnMake() is a no-op. Even the MakeNextCapture() is usually not more than remembering which piece is on the target square after the move (and perhaps activating X-ray pieces that were behind the used piece by adding them to the list). So we get:

Code: Select all

int SEE(int apha, int beta, int eval, int victim)
{
  if(eval >= beta) return beta; // stand pat produces (fail-hard) cutoff
  if(eval > alpha) alpha = eval; // this is the crucial alpha update
  eval += PieceValue[victim];
  victim = NextPieceOfAttackersList(); // attacker is now on target square
  alpha = -SEE(-beta, -alpha, -eval, victim);
  return alpha;
}
This negamax formulation still involves flipping the sign of alpha a number of times while unwinding; to make it into an iterative version it is thus more efficient to 'unroll' that iteration in pairs, so that white and black use different code. As preparation for this, first the recursive version:

Code: Select all

int MySEE(int apha, int beta, int eval, int victim)
{
  if(eval >= beta) return beta; // stand pat produces (fail-hard) cutoff
  if(eval > alpha) alpha = eval; // this is the crucial alpha update
  eval += PieceValue[victim];
  victim = NextPieceOfMyAttackersList(); // attacker is now on target square
  return HisSEE(beta, alpha, eval, victim);
}

int HisSEE(int apha, int beta, int eval, int victim)
{
  if&#40;eval <= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < alpha&#41; alpha = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  return MySEE&#40;beta, alpha, eval, victim&#41;;
&#125;
So we made a HisSEE version for the opponent where alpha, beta and eval by convention all use opposite signs from those in MySEE. This means also that all comparisons have been flpped to the opposite comparison there (i.e. >= becomes <=, etc.). Finally we swap the dummy parameter names alpha and beta in HisSee, to harmonize the naming with that in MySEE (which was calling HisSEE with actual value alpha for the formal parameter beta, and vice versa):

Code: Select all

int HisSEE&#40;int beta, int alpha, int eval, int victim&#41;
&#123;
  if&#40;eval <= alpha&#41; return alpha; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  return MySEE&#40;alpha, beta, eval, victim&#41;;
&#125;
So now we have a true tail recursion, which can be trivially converted to an iteraton, where every internal return simply breaks out of the loop, and the return at the end starts the next iteration:

Code: Select all

eval = 0;
while&#40;1&#41;
&#123;
  // MySEE part
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  // HisSee part
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
&#125;
A slight optimization puts the stand-pat tests to before 'making' the next move, corresponding to the well-known futility-pruning in the recursive implementation:

Code: Select all

eval = 0;
while&#40;1&#41;
&#123;
  // MySEE part
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  // HisSee part
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
&#125;
This made the taking of the next piece from 'his' list of attackers wrap around to the previous iteration, which can be corrected by putting the original content of the square as first element in this list. An aternative would be to skip the first three statements on the first iteration: the comparison for cut-off is not likely to be useful there (or coud be done outide of the loop) as we want to start with beta = INFINITE in the SEE root. And we do not want to allow stand-pat before the side to move actually captures anything; the first move must be forced (and not necessarily by the LVA). This transforms the loop to:

Code: Select all

eval = 0;
victim = board&#91;targetSquare&#93;;
while&#40;1&#41;
&#123;
  eval += PieceValue&#91;victim&#93;;
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
&#125;
So the bottom line is that proper implementation of alpha-beta only adds a single conditional move to each original (i.e. non-unrolled) iteration of the SEE loop, to update alpha or beta. I don't think that would be a very large computational burdon.
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: SEE algorithm on chessprogramming wiki

Post by bob »

hgm wrote:I don't think this issue can be fully clear without exactly seeing what is involved in the itertive version. So let me have a stab at this:

This is the regular alpha-beta search as applied to SEE, where there is no loop over moves, as at any stage we only try stand pat or the capture to the given target with the lowest attacker:

Code: Select all

int SEE&#40;int apha, int beta, int eval&#41;
&#123;
  if&#40;eval >= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  // FOR ALL MOVES
  MakeNextCapture&#40;);
  score = -SEE&#40;-beta, -alpha, -&#40;eval + PieceValue&#91;victim&#93;));
  UnMake&#40;);
  if&#40;score > alpha&#41; alpha = score;
  // NEXT MOVE
  return alpha;
&#125;
Note that with fail-hard, the returned score will be at least alpha, so the update of alpha can be made unconditionally, and the code becomes:

Code: Select all

int SEE&#40;int apha, int beta, int eval&#41;
&#123;
  if&#40;eval >= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  MakeNextCapture&#40;);
  alpha = -SEE&#40;-beta, -alpha, -&#40;eval + PieceValue&#91;victim&#93;));
  UnMake&#40;);
  return alpha;
&#125;
Now note that this is a 'tail recursion' except for the UnMake(). So after we reach the deepest level, the whole thing unwinds by doing a series of UnMakes. Now the representation we use during a SEE is usually not a board, but some sorted lists of captures to the given square, made at the beginning. And we do not want to use these lists again; after we are done wth the SEE, we will just discard them. So there really is no reason to UnMake() anything at all, and UnMake() is a no-op. Even the MakeNextCapture() is usually not more than remembering which piece is on the target square after the move (and perhaps activating X-ray pieces that were behind the used piece by adding them to the list). So we get:

Code: Select all

int SEE&#40;int apha, int beta, int eval, int victim&#41;
&#123;
  if&#40;eval >= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  victim = NextPieceOfAttackersList&#40;); // attacker is now on target square
  alpha = -SEE&#40;-beta, -alpha, -eval, victim&#41;;
  return alpha;
&#125;
This negamax formulation still involves flipping the sign of alpha a number of times while unwinding; to make it into an iterative version it is thus more efficient to 'unroll' that iteration in pairs, so that white and black use different code. As preparation for this, first the recursive version:

Code: Select all

int MySEE&#40;int apha, int beta, int eval, int victim&#41;
&#123;
  if&#40;eval >= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  return HisSEE&#40;beta, alpha, eval, victim&#41;;
&#125;

int HisSEE&#40;int apha, int beta, int eval, int victim&#41;
&#123;
  if&#40;eval <= beta&#41; return beta; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < alpha&#41; alpha = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  return MySEE&#40;beta, alpha, eval, victim&#41;;
&#125;
So we made a HisSEE version for the opponent where alpha, beta and eval by convention all use opposite signs from those in MySEE. This means also that all comparisons have been flpped to the opposite comparison there (i.e. >= becomes <=, etc.). Finally we swap the dummy parameter names alpha and beta in HisSee, to harmonize the naming with that in MySEE (which was calling HisSEE with actual value alpha for the formal parameter beta, and vice versa):

Code: Select all

int HisSEE&#40;int beta, int alpha, int eval, int victim&#41;
&#123;
  if&#40;eval <= alpha&#41; return alpha; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  return MySEE&#40;alpha, beta, eval, victim&#41;;
&#125;
So now we have a true tail recursion, which can be trivially converted to an iteraton, where every internal return simply breaks out of the loop, and the return at the end starts the next iteration:

Code: Select all

eval = 0;
while&#40;1&#41;
&#123;
  // MySEE part
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  // HisSee part
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
&#125;
A slight optimization puts the stand-pat tests to before 'making' the next move, corresponding to the well-known futility-pruning in the recursive implementation:

Code: Select all

eval = 0;
while&#40;1&#41;
&#123;
  // MySEE part
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
  eval += PieceValue&#91;victim&#93;;
  // HisSee part
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
&#125;
This made the taking of the next piece from 'his' list of attackers wrap around to the previous iteration, which can be corrected by putting the original content of the square as first element in this list. An aternative would be to skip the first three statements on the first iteration: the comparison for cut-off is not likely to be useful there (or coud be done outide of the loop) as we want to start with beta = INFINITE in the SEE root. And we do not want to allow stand-pat before the side to move actually captures anything; the first move must be forced (and not necessarily by the LVA). This transforms the loop to:

Code: Select all

eval = 0;
victim = board&#91;targetSquare&#93;;
while&#40;1&#41;
&#123;
  eval += PieceValue&#91;victim&#93;;
  if&#40;eval <= alpha&#41; &#123; result = alpha; break; &#125; // stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfMyAttackersList&#40;); // attacker is now on target square
  if&#40;eval < beta&#41; beta = eval; // this is the crucial alpha update
  eval -= PieceValue&#91;victim&#93;;
  if&#40;eval >= beta&#41; &#123;result = beta; break; &#125;// stand pat produces &#40;fail-hard&#41; cutoff
  victim = NextPieceOfHisAttackersList&#40;); // attacker is now on target square
  if&#40;eval > alpha&#41; alpha = eval; // this is the crucial alpha update
&#125;
So the bottom line is that proper implementation of alpha-beta only adds a single conditional move to each original (i.e. non-unrolled) iteration of the SEE loop, to update alpha or beta. I don't think that would be a very large computational burdon.
The issue is the procedure call. Which is not optimized away by the compiler, and which is an expensive operation. If the compiler would turn it into an interative version, that would be nice, but I have not seen any that would even attempt this. That's why I've always just coded it as a loop.
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: SEE algorithm on chessprogramming wiki

Post by bob »

michiguel wrote:
bob wrote:
mcostalba wrote:
bob wrote: This has very low cost.
Yes, I agree.

One interesting problem is to see if there is some benefit in adding the burden to detect captures by pinned pieces.

I have experimented a bit some months ago with this, but I have no found any measurable gain in real games, so I dropped also this one :-(
I tried this a year or two ago when it first came up. I first ran a bunch of fixed-depth games, and modified SEE to just blindly look for pinned pieces and factor in the cost of moving that piece in addition to how it contributes to the capture. I didn't try to do this efficiently because I was not sure the idea was worth anything. Using fixed depth, I found no difference in old and new. But for my implementation, new was certainly expensive. Net result would have been horrible if I had tried using time. But with no measurable benefit, I simply dropped the idea. SEE can be wrong for other reasons just as frequently, including using overloaded pieces, or moving a piece that is not "directly pinned" but which is blocking a key attack on a square that is important. I think there's enough errors that eliminating just pinned pieces is not going to help enough to make it worthwhile, even if it can be done very fast.
To the moderators: Don Dailey has hijacked Bob's account :-)

Miguel
Not quite. :)

The point here was that someone suggested that evaluating pins might help make the search more accurate since SEE is often used to prune captures in the q-search. I was pretty sure it would not make any significant difference since the entire q-search is already extremely error-prone. To answer the question that was asked, I simply did a working implementation, without regard to speed at all, since I wanted to answer the question with minimal effort. Hence the test. If I had tested the way it _should_ have been tested, I would have spent significant time trying to make it efficient, even though I was almost certain the idea would not be beneficial.

In the normal course of events in Crafty development, I don't try things that I am nearly certain won't help, I only implement things that seem logical/reasonable and then I spend the necessary time to come up with an efficient implementation as well. In this case, this seemed like the easiest way to show that the idea was not helpful, with minimal effort. And the accuracy of this test is _still_ in question since changing SEE changes the shape of the tree as well, which really needs timed testing, not depth. But it was "good enough" to answer a question I was pretty sure I already knew the answer to. This was the second fixed-depth test I have run in the past 5 years (that I can recall). The first was to simply verify that my cluster opponents had no random component in their evaluation that would change the results unexpectedly.
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: SEE algorithm on chessprogramming wiki

Post by hgm »

bob wrote:The issue is the procedure call. Which is not optimized away by the compiler, and which is an expensive operation. If the compiler would turn it into an interative version, that would be nice, but I have not seen any that would even attempt this. That's why I've always just coded it as a loop.
Not sure what you are talking about. Seems to me the issue is a conditional move, not a procedure call...
bob
Posts: 20943
Joined: Mon Feb 27, 2006 7:30 pm
Location: Birmingham, AL

Re: SEE algorithm on chessprogramming wiki

Post by bob »

hgm wrote:
bob wrote:The issue is the procedure call. Which is not optimized away by the compiler, and which is an expensive operation. If the compiler would turn it into an interative version, that would be nice, but I have not seen any that would even attempt this. That's why I've always just coded it as a loop.
Not sure what you are talking about. Seems to me the issue is a conditional move, not a procedure call...
Here's the part of my previous post I was commenting on:
bob wrote: That's why I have never used a recursive implementation even though the code becomes a bit simpler. And a bit slower.
A recursive implementation depends on procedure calls and returns, which is less efficient because of the call instruction in X86. An iterative approach (which I have always used) avoids this overhead completely. I have tried this recursive approach, just for fun, but there is a small speed penalty. But since this gets executed so frequently, it has a measurable impact on overall speed.