I have now fully written the code for minimaxing the score over Duck move (of opponent) + FIDE move, which is going to replace the usual
if(score > alpha) { score = alpha; if(score >= beta) goto cutoff; }
I am not very happy with it, because it has a significant 'boiler plate' character: I distinguish the cases where there are zero, one or many squares where the move can be blocked. And in each case more or less the same is then done, except that the test for whether a given square could block the move is very different. This is because the test I use for determining whether a distant slider move is blocked at a given square is a bit cumbersome, and it seemed a waste to perform it instead of the very simple test for whether a 'contact move' is blocked (which is just comparing with the single square where it could be blocked), or skipping any test whatsoever (becasue the move was unblockable to begin with). I suppose this could be a form of premature optimization...
Anyway, to test for move blocking I make use of two 240-byte tables, which give me the distance between two squares on the same ray (or 1 for a Knight jump), and a unique code for the direction. I index those by toSqr-fromSqr, as the engine uses 0x88 square numbering (16*rank + file). Before 'looping over Duck locations' I look those up for the move at hand. And I decrement the distance when that move is a capture, as what I really want to know is the number of squares where the move can be blocked. To test whether a Duck location is in the path of the move I then look up the direction for going from fromSqr to duckSqr, and only if it is the same as that of the move I look up the distance between fromSqr and duckSqr. That tells me whether the move can be blocked on duckSqr.
I know ways to do this with less code, but only at the expense of much larger tables, and I am a firm believer of economizing on L1 cache space. The result of the preliminary lookups (done once per move) in any case tell me the number of blocking squares, and when this is 1 I just remember that square, and compare the duckSqr to it in the loop over Duck moves. While in case there are 0 I do the same loop without testing for anything.
The code assumes the previous call to Search returned variables score, exScore and exSqr (the first two already flipped in sign as negamax requires). The latter two are the 'exception' for the single Duck location (at exSqr) where we cannot get the best result because we were not allowed to leave the Duck were we needed it most. For all other Duck locations we can move the Duck to the best location, so they all get the same score 'score'. When there is no exception (because the two best Duck locations have the same effect, which often happens when the best reply cannot be blocked or can be blocked in more than one way), exSqr will be returned as -1. (Which is an invalid square number.)
Each node declares local arrays exScores[], next[] and prev[] indexed by square number (so for me 120 elements, as I use 0x88 numbering), which are going to keep track of the best score so far for each of the 64 Duck locations at the start of the FIDE move. To economize on the effort of updating this we make use of the fact that at least 56 of these Duck locations will result in the same score, namely that of the best move (so far), as moves can be blocked in at most 7 squares, and return a single exceptional score themselves. So we keep up to 8 exceptions in a (circular) doubly-linked list, where the arrays next[] and prev[] hold the links. All Duck locations not in the list will have the same score ('bestScore').
To update the scores array we first run through the list of existing exceptions, and delete any of those which cease to be exceptions because their score is raised to the common score by the current move. This raising can of course only happen when the exception in question would not block the current move. So we have to test for blocking in the loop over Duck locations (i.e. while stepping through the exception list using the next[] links), and this also allows us to keep track of the blocking squares we already have processed (as a bit mask). We also have to test if a Duck location in the exception list is the exception returned by the move (as this will have to use another score in the update), and (when we do encounter it) flag that it already has been updated, by setting exSqr invalid.
After this we then have to loop through remaining blocking squares and the returned exception (if that remained), as these become new exceptions. But they only do so when the move did raise the common score. Which hopefully is not too often, if the move ordering doesn't suck. Of course in that case the common bestScore will also be updated. This whole procedure then gives the best result for each of the 64 possible Duck locations (just before the FIDE move) from all moves that have been searched so far.
Now the opponent will place the Duck in the location with the lowest such score (if he can), or with the second lowest score (in the single case he cannot). So while we are updating the exceptions we also keep track of their minimum. That minimum becomes the 'score' returned by the node, the Duck location that has this score is returned as exSqr, and the second-lowest score as exScore. But the latter two data items are only relevant after all moves have been searched (or we hit a beta cutoff), and are ready to return. In the move loop, however, we do need the lowest score for updating alpha, and deciding whether to take a beta cutoff (if alpha >= beta). That finally gave me the following code:
Code: Select all
int lowest = beta;
int moveDir = direct[to-from]; // direction code (step vector)...
int moveDist = rayLen[to-from] - (victim != 0); // ... and length of free path that move traversed
int k = 8, mask = 0; // mask represents squares on ray where Duck could block move
if(moveDist > 1) { // slider move through 2 or more empty squares
while((k = next[k]) != 8) { // loop through existing exceptions
int d = dist[to-from];
if(d > moveDist || direct[k-from] != moveDir) { // move allowed for this Duck location
if(k == exSqr) { // returned exception already existed
if(exScores[k] < exScore) exScores[k] = exScore;// raise its score if needed
exSqr = -1; // kludge to flag returned exception is processed
} else if(score >= bestScore) {
next[prev[k]] = next[k], prev[next[k]] = prev[k]; // ceases to be exception
exScores[k] = score;
} else { // score < bestScore; exceptions remain exceptions, but can still benefit
if(exScores[k] < score) exScores[k] = score;// raise if needed
}
if(exScores[k] < lowest) lowest = exScores[k];
} else mask |= 1 << d; // move blocked, but the blocking does not create a new exception
}
if(score > bestScore) {
mask = (1 << moveDist) - 1 - (mask >> 1); // find remaining cases of blocking
if(mask && bestScore < lowest) lowest = bestScore;
while(mask) { // make all these new exceptions
int n = BSF(mask);
k = from + (n + 1)*moveDir; // blocking square
exScores[k] = bestScore; // keeps (old) bestScore
prev[k] = 8; next[k] = next[8]; // add exception to list
next[8] = prev[next[8]] = k;
if(k = exSqr) exSqr = -1; // forget the returned exception, as it blocks the move.
mask &= mask - 1;
}
if(exSqr >= 0) { // the returned exceception did not exist yet, and does not block the move
exScores[k] = exScore; // give it the returned score
prev[k] = 8; next[k] = next[8]; // and add it to the list
next[8] = prev[next[8]] = k;
if(exScore < lowest) lowest = exScore;
}
bestScore = score;
}
} else if(moveDist == 1) { // single blocking square
int block = from + steps[moveDir-1]; // calculate it (needs not be to-square in case of capture)
while((k = next[k]) != 8) { // loop through existing exceptions
if(k != block) { // move allowed for this Duck location
if(k == exSqr) { // returned exception already existed
if(exScores[k] < exScore) exScores[k] = exScore;// raise its score if needed
exSqr = -1; // kludge to flag returned exception is processed
} else if(score >= bestScore) {
next[prev[k]] = next[k], prev[next[k]] = prev[k]; // ceases to be exception
exScores[k] = score;
} else { // score < bestScore; exceptions remain exceptions, but can still benefit
if(exScores[k] < score) exScores[k] = score;// raise if needed
}
if(exScores[k] < lowest) lowest = exScores[k];
} else block = -1; // move blocked, but the blocking does not create a new exception
}
if(score > bestScore) {
if(block >= 0) { // a new exception should be made for the blocked case
exScores[block] = bestScore; // old value!
prev[block] = 8; next[block] = next[8]; // add exception to list
next[8] = prev[next[8]] = block;
if(bestScore < lowest) lowest = bestScore;
}
if(exSqr >= 0 && exSqr != block) { // a new exception should be made for the returned exception
exScores[exSqr] = exScore; // which has its own value!
prev[exSqr] = 8; next[exSqr] = next8]; // add exception to list
next[8] = prev[next[8]] = exSqr;
if(exScore < lowest) lowest = exScore;
}
bestScore = score;
}
} else { // move cannot be blocked (contact capture)
while((k = next[k]) != 8) { // loop through existing exceptions
if(k == exSqr) {
if(exScores[k] < exScore) exScores[k] = exScore;// raise its score if needed
exSqr = -1; // kludge to flag returned exception is processed
} if(score >= bestScore) {
next[prev[k]] = next[k], prev[next[k]] = prev[k]; // ceases to be exception
exScores[k] = score;
} else { // score < bestScore; exceptions remain exceptions, but can still benefit
if(exScores[k] < score) exScores[k] = score; // raise if needed
}
if(exScores[k] < lowest) lowest = exScores[k];
}
if(exSqr >= 0) { // a new exception should be made for the returned exception
exScores[exSqr] = exScore; // which has its own value!
prev[exSqr] = 8; next[exSqr] = next[8]; // add exception to list
next[8] = prev[next[8]] = exSqr;
if(exScore < lowest) lowest = exScore;
}
if(score > bestScore) bestScore = score;
}
if(bestScore < lowest) lowest = bestScore;
if(lowest > alpha) {
alpha = lowest;
if(lowest >= beta) gotoCutoff;
// stuff for updating or printing PV goes here
}