Chinese Chess

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

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

Chinese Chess

Post by hgm »

It has been years since I worked on my Xiangqi engine 'HaQiKi D'. But this year the ICGA Computer Olympiad is in TaiPei, which means there will be far more participants for Xiangqi than usual. So a good occasion to put some more effort in it. (Surprisingly enough there also is a lot of interest for mini-Shogi there, which I had imagined to be a typically Japanese game.)

The current HaQiKi D is a simple mailbox engine, storing piece numbers in the board cells, which are the index of the corresponding piece in the piece list. Xiangqi does not have real promotion, so there can never be more pieces of any type as you have initially, and they are then either present or captured. The only tricky thing compared to Chess is that the moves for some piece types are dependent on their location on the board: Pawns get extra moves on the opponent half ('across the River'). The confinement of King and Advisors to the Palace and Elephants to their own half can also be implemented this way, by not giving them the moves that would step out of their confinement zone in squares at the edge of this zone. So rather than having a single (zero-terminated) list of possible board steps for each piece type, there are now many listst, and a board-sized table for each type indicates which list should be used for each square. E.g. a Rook on a0 would use the list with only a forward and rightward step. This actualy saves some time during move generation, as you would never try moves that would bring you off board immediately. (But at the expense of some L1 cache space, which I minimized by interleaving the tables for pieces with limited board access.)

Due to the success of the incrementally updated attack map in my Tenjiku Shogi engine, (where it gave an incredible speedup), I would like to use a similar scheme now for HaQiKi D. Xiangqi introduces some new aspects, which have to be dealt with, though. Instead of limited-range sliders it has 'lame leapers': Elephants and Horses can be blocked on a square they cannot move to. In addition, the Horse is a 'bifurcating' lame leaper: if it is not blocked, it can go on in two directions from the potential blocking square. Finally there are the Cannons, which do not attack their nearest neighbors, but their second-nearest neighbors. Superficially this is somewhat like the jumping generals of Tenjiku Shogi, but in practice it is very different, as they have to overjump exactly one piece, so that their possible capture target changes by interposing or withdrawing a piece.

The attack map

All bits in the attack map will correspond to individual pieces (rather than piece types or directions). The reason is that both Cannons and Rooks can simultaneously attack the same square from the same direction, and thus need separate bits. For efficient incremental update of the map it will also be necessary to record 'blocking squares', where mutation of the occupancy would affect a real attack elsewhere. These can be the squares adjacent to Horses and Elephants (where they can be blocked, but not go), or the 'mount' of a Cannon (which must be overjumped for capturing). So each Elephant, Horse and Cannon will have two bits in the attack-map word for a given square. This brings the number of bits to 22 (16 real attacks, 6 blocks). In theory this could be economized on, as Pawns could never go where Kings or Advisers can go, but as long as it fits a 32-bit words, there seems no advantage in that. It does imply that the attacks of each player will reside in a different word, but usually you need only one or the other anyway.

The attack map elements will be valid for slider attacks (and blocks) only on occupied squares, and for leaper attacks (and blocks) on all squares. This means that moving a Horse or Elephant wil always involve setting the block bits in 4 of its neighbor squares (or clearing them for the old location). If the square is empty, it will then also set (or clear) the attack bits of the squares that could be reached over it. For a moved Cannon the nearest neighbor is the mount, and will get the block bit for that Cannon set, while the second neighbor in that direction will get the Cannon's attack bit set. because of the use of a second neighbor we must make sure that boundary cells of the neighbor table will point to something sensible even in the off-board directions. (E.g. to a dummy square, or perhaps to themselves.)

When a square gets evacuated, the attack-map element for it then will indicate all moves that are discoverd by this. Leaper attacks can be ignored, but a leaper block will now have to discover the corresponding leaper move (one for an Elephant, two for a Horse), irrespective of whether it goes to an occupied or empty square. Evacuating a Cannon mount will change the downstream attack into the new mount, and displace the attack to the neighbor beyond that. For a Rook we simply have to displace the attack downstream, to the nearest neighbor in that direction. The downstream neighbors will be obtained from the neighbor table. A complicating factor here is that some piece types need modification of two attack-map elements, when discovered, while others only need to modify one.

Discovering moves on square evacuation

The set bits can be extracted from the attack-map element for an evacuated square one by one. As each bit corresponds to a particular piece, we would have to find the location of that piece (from the piece list) in order to know the direction the piece came from. As leaper bits will be already masked off before the bit extraction, we will now have to apply the discovered attacks going over the evacuated square in that direction to the attack map at the target squares. This could of course be done with a switch statement (or branch tree) to identify the type of the involved piece, but such programming constructs are sure to give large numbers of mispredictions. So my first inclination is to use branch-free code for this. That means the code must be able to handle the worst case, which is altering of two attack-map elements (for Cannons and Horses). That means that in some cases (Rooks and Elephants) the second change should be turned into a dummy, and will be wasted effort.

Code: Select all

int todo = attackMap[color][sqr] & ~LEAPER_ATTACKS;
while(todo) {
  int n = lowBit[todo];                           // extract bit
  int piece = nr2piece[n] + color;                // index in piece list
  int s = location[piece];                        // square it is on
  int dir = direction[sqr - s] + typeOffset[n];   // (quasi-)direction of blocked move
  int target = neighbor[sqr][dir];                // first target to change
  attackMap[color][target] ^= change1[n];         // apply change
  dir &= 7;
  target = neigbor[target][dir];                  // second target to change
  attackMap[color][target] ^= change2[n];         // apply change
  todo -= 1<<n;                                   // done
}
This code is written as it should be for Cannons: the first nearest neighbor in the direction of attack is the new mount, and the neighbor of that in the same direction the newly attacked square. (This because orthogonal directions are < 8, and thus not changed by the "&= 7" kludge.) For a Cannon change2[] then contains its attack bit, and change1[n] both the attack and the block bit.

For Rooks and Elephants, the second change is turned into a no-op by having their change2[n] = 0. The kludgy part of the code comes from the way directions are numbered: 8-11 are diagonal directions, 4-7 orthogonal directions. The latter refer to the directions for which we keep true neighbor information in the neighbor table. As Xiangqi has no diagonal sliders, there is no need to keep track of diagonal neighbors! The diagonal part of the neighbor table is only written at startup, as if the adjacent square in the given diagonal direction is the nearest neighbor. Displacing the attack to the nearest neighbor in change1[n] will thus apply it to the Elephant target.

For the Horses we use a similar kludge. But their blocking squares ly in orthogonal directions, like those of Rooks and Cannons. To make the distiction, we added typeOffset[n] to the direction obtained from the table, which is 4 for Horses, and 0 otherwise. This turns the orthogonal direction into the (clockwise most adjacent) diagonal direction, so the first target will be taken to ly one diagonal step away from theblocking square (i.e. a Knight jump away from the Horse). The dir &= 7 then turns directions 8-11 into 0-3. The neighbor table for these directions is initialized (and unchanging) to squares two orthogonal steps away, needed to get from one Horse target to its partner (blocked on the same square), i.e. perpendicular to the original orthogonal direction.
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Chinese Chess

Post by hgm »

Capture generation

The attack map makes it easy to generate captures-only, and even makes it possible to generate captures in order of (decreasing) victim value. That eliminates most of the move sorting for captures, and makes staged move generation possible. Futility pruning can be done 'in bulk', before the moves to be pruned are even generated; you simply stop generating new moves when you reach the victims that are not valuable enough to surpass alpha - MARGIN.

To generate the captures on the victim whose 'stage' has come up, we can do:

Code: Select all

int victim;
for(victim = MOST_VALUABLE_PIECE; victim > FUTILITY_LIMIT; victim = Next(victim)) {
  int toSqr = location[victim];
  if(toSqr == INVALID) continue;                    // piece is captured
  int todo = attackMap[color][toSqr] & ~BLOCK_FLAGS;
  while(todo) {
    int n = lowBit[todo];                           // extract bit
    int piece = nr2piece[n] + color;                // index in piece list
    int fromSqr = location[piece];                  // square it is on
    AddMove(fromSqr, toSqr);                        // or search it directly
    todo -= 1<<n;                                   // done
  }
}
The attack bits can be assigned so that they are extracted in order of increasing piece value, to get MVV/LVA ordering: the lowest-order bits for Pawns, then Advisors, then Elephants, then the King, then Horses, then Cannons and then Rooks. The King seems in the wrong place here, but as King can only capture unprotected pieces, for which LVA ordering isn't really an advantage, this should not matter. It was put there to make all leaper attacks contiguous, so that they can be 'masked off' by a right shift when using the attack map for discovering moves. Note that Advisors never compete with either Elephants or Pawns. Elephants can compete with Pawns on only 2 squares, and these are then never passed Pawns (i.e. across the River), so that they are worth less than an Elephant. (Passed Pawns in general are worth more.)

There is one problem, however: there can be two pieces of each type, and if both are attacked, extracting the moves in LVA order on one first, and then on the other, doesn't result in proper MVV/LVA ordering. We could try capturing a Cannon with a Rook before capturing a Cannon with a Pawn, because it was the other Cannon (that we did not start with) that happened to be attacked by the Pawn. This gets even worth if there are multiple types in the same 'value group', such as Advisors and Elephants, and perhaps Horses and Cannons. (Although I am inclined to consider Horse < Cannon, as it seems important to tune the algorithm to early in the game. As in the end-game, where Horse > Cannon, the board is so thinly populated that multiple captures to the same square do not often occur, so that ordering them becomes less critical. Plus that the game then usually is decided already anyway.)

So we would have to generate all captures on a certain type or value group, and then still sort these LVA-wise. We could get rid of that sorting if we generate by type: if we assign the attack bits only to the even bits in the attack map (which is still possible in a 32-bit word, as there are only 16 pieces), using odd bits only for the block flags, (which in any case would have to be masked out before extracting captures), we can easily interleave two attack sets by totalSet = 2*set1 + set2;, and then extract the attacks from totalSet in combined LVA order. A single instruction for combining the sets could then remove the need for any post-generation sorting entirely.

This would not work for value groups of 4 pieces (Advisors + Elephants) or 5 pieces (Pawns). Note, however, that captures of Advisors and Elephants are always HxL captures; Pawns are worth at least as much as a single A or E by the time they can capture it. From the attack map it can be easily seen if a piece is protected (although this does not take into account whether the 'protecting' piece is pinned, i.e. is a false protector). When we would not immediately search such captures (which are bad, or at least suspect), but stash them away until all good or equal captures have been generated, we are automatically sure that all captures with Pawns, Advisors or Elephants have been tried before we try something seriously bad. (Which we might prune altogether.) So if we generate all captures on Elephants before those on Advisors, all but those of the (approximately equal-vaued) Pawns would be searched (all PxE before PxA), or those of unprotected E or A (where we don't care about the value of the attacker), and only then (H, C or R) x protected (A or E) will be considered. Which of the latter deserves to be searched first (if at all) depends on whether there is a second attacker and what is the value of the protecting piece just as much as on the value of the attacker, i.e. on the SEE value, and LVA is insufficiant guidance. And it is questionable how important SEE really is here, as we already took account of the most important aspect, namely whether the victim is protected or not. The issue is thus reduced to how the dubious captures should be sorted.

Pinning

What seems more important is to recognize false protectors, as in Xiangqi Advisors and Elephants typically protect each other, but also are frequently pinned by Rooks or Cannons along the central file or backrank. So it seems advantageous to detect pinned pieces of both players, so their attacks can be masked out of the attack-map elements. In Xiangqi Rooks, Cannons and Horses can all pin pieces against the King; these can be detected by testing all 6 of those for alignment with the enemy King in 0x88 style, and then using the neighbor table to see if they are second (Rooks) or third (Cannon) neighbor in the detected direction (in which case any pieces of the King player in between are pinned), or whether a square diagonal from King in the relevant direction is occupied (Horses). As far as pinning is concerned, a King acts like a Rook because of the King-facing rule.

The attack bits of pinned pieces cannot be unconditionally masked out of the attack-map elements, though, as even pinned pieces can still have moves: capture of the piece that pins them would resolve the pin. Xiangqi is more complex than Chess here, because pieces can be multiply pinned! E.g. a Rook or Cannon can be pinned by an enemy Rook, with an enemy Cannon behind it. Withdrawing the pinned piece would cause both the Rook and the Cannon to check, and you can only capture one at the time. A Rook could capture the Rook, but then would cause the Cannon to check (as it now is the only piece between Cannon and King, thus acting as mount). A Cannon could jump over the Rook to capture the Cannon, but that would leave nothing between the Rook and King. (The Rook could be replaced here by a King for the same effect.) So multiple pins are unresolvable. (The pinned piece could still move along the pin ray without capturing, but we are only interested in captures at this stage.) Two Horses can also cause a double pin of a piece diagonally adjacent to the King. If that piece is a Rook, it cannot resolve the pin by capturing its pinner, which would be possible for a Rook pinned by a single Horse.

Multiply-pinned pieces can thus be unconditionally masked out of the attack-map elements before these are used for generating captures, or deciding if a piece is protected. But a singly pinned piece can not. For generating captures it is not so bad if there are illegal ones amongst those; it will be quickly detected that they create a capture on their King when discovering opponent moves, so that we can abort the move before much work has been spent on it. Failing to recognize a false protector can lead to searching moves in a wrong order, and finding a cut move that could have been obvious (e.g. R x falsely-protected Cannon) only after much wasted search effort. In general this is a hairy problem, however. Because we would need to know if the protector is pinned after the capture of the piece it seems to protect. E.g. we could have on a file: white Cannon, white Rook, black Horse, black Rook, black King. Superficially there is no pin, as there are two pieces between wR and bK, and 3 pieces between wC and bK. If white plays RxR, however, this leaves only two pieces between wC and bK, namely wR and bR, so the bR is pinned and cannot recapture. The problem here is that the initial capture discovers a pin.

But I guess (hope...) that situations like this are relatively rare; in Chess they can occur as well. A full SEE taking account of pinning would detect them, of course, but is rather expensive. In the common case enemy pieces that are pinned before I capture something that they pseudo-legally protect will remain pinned. So it is not too crazy to completel ignore protection by pinnend pieces. A refinement could be to mark (during pin detection) the squares on which there are Horses, Cannons, Rooks, Knight or King by setting the attack bits for the pieces they pin on an auxiliary board (in the local node data) and then, when such a piece moves, OR the mask used to clear attack bits of false protectors with this bit set before ANDing it with the attack map. This assumes making a capture with the pinner will break the pin. Which would not be true for doubly-pinned pieces. But these could be detected too, and their bits could be removed from the bit sets of their pinners. This would be pretty accurate for detecting when pins are broken, but the reverse error, where a pin of an originally good protector gets discovered through the capture, would still occur.
User avatar
phhnguyen
Posts: 1434
Joined: Wed Apr 21, 2010 4:58 am
Location: Australia
Full name: Nguyen Hong Pham

Re: Chinese Chess

Post by phhnguyen »

I am not discussing about your techniques but about advantages / benefits of using attack maps in Xiangqi since I have tried already once for my old mailbox engine.

Here is my thoughts:

1. Difficulties:
- Functions for incrementally updated attack maps are usually very complicated, buggy and hard to make it be fast enough. Need a lot of effort to debug and optimise.

2. Advantages / benefits:
a) We can generate move much faster (once we have attacking maps)
b) They could help to rule / judge permanent chases
c) They could provide some good information such as pinnings, number of attackers... to Evaluation function

I have tried using attack maps mainly because of b) and c).

However I have faced some problems:
a) Significant slower (for searching)
- As you have mentioned, Xiangqi has some tricky pieces such as Cannon, blockable Horse, Elephant... They all make harder for updating attack maps. From my experience, time for that updating function is quite close to generating all moves. After each move you need to update two attacking maps for both sides, it is similar to call twice move generator to generate all moves
- All tricks to avoid calling move generators such as using moves from hash table, history moves... become useless or less effective since you have already generated all moves (indirectly)

b) Attack maps may help ruling permanent chases. Unfortunately it is overdone. To avoid re-computing attack maps, we must store those maps for each moves, make our apps consume more memory and be easier to miss caches. The proportion of permanent chasing moves is quite small. It means generating what we need on fly is much faster.

c) Attack maps may help evaluation function. Unfortunately it is overdone too since I have tried but cannot use all but only few information from attack maps. Thus, again, generating what I need on fly is much faster.

My (5 cents) conclusions:
- Creating any information we need on fly is much cheaper and more convenient than using attacking maps
- If we insist something like attacking maps, using bitboard to create them on fly is faster and easier than creating and updating increasingly them with mailboxes
https://banksiagui.com
The most features chess GUI, based on opensource Banksia - the chess tournament manager
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Chinese Chess

Post by hgm »

phhnguyen wrote: Sun May 27, 2018 7:06 amFrom my experience, time for that updating function is quite close to generating all moves. After each move you need to update two attacking maps for both sides, it is similar to call twice move generator to generate all moves
This sounds very wrong. The time for updating the attack map should be close to that for generating captures for two pieces, namely the moved piece from its old location (to remove the old attacks) and its new location. E.g. if you move a Rook, you will have to remove its 4 old attacks and add 4 new attacks (assuming you don't make use of the fact that attacks along the ray it moves on remain, and only the perpendicular attacks need to be altered). That should be as much work as generating captures for two Rooks, and thus saves you the work on the remaining 14 pieces.

Code: Select all

// Rook case
bit = piece2bit[piece];
for(dir=4; dir<8; dir++) {        // orthogonal directions
  int s = neighbor[fromSqr][dir]; // capture target
  map[color][s] ^= bit;           // add/remove attack
}
You might have to do it twice, though, also for restoring the old map on UnMake(). That still means 4 pieces versus 16. The work for discovering moves over the from-square has to be added to that, but should be rare. The common case is that you look at the attack-map element for the from-square, conclude that there were no attacks to it that would need to be discovered, and you are done.

In addition, you don't have to do the full update in ever node, but only in nodes where you are actually going to make moves. E.g. if (lazy) eval is far below alpha, and you do not attack any pieces with a value large enough to get above alpha-MARGIN, you will not search any moves, and can in fact return a fail-low immediately. (In d=1 or leaf nodes, that is, but non-leaf nodes make up an almost negligible fraction of the tree.) To know if you have any captures you would only have to update your own attack map for the mutations caused by the preceding move of the opponent. You only have to remove the attacks of your piece that was captured for that. (But it is cheaper to do that by masking out the attacks of that piece whenever you consult the attack map, as you have to mask out the blocking bits anyway, so that you can do this by just including the attack bit for the captured piece in the mask you use for that. And you don't have to UnMake() anything then.) And add your own attacks that the previous move discovered (i.e. when he moved a soft-pinned piece). Which only rarely exist. (It is much more common you were blocking your own pieces than that the opponent was blocking them.)

So the common case (when you cannot stand pat) would be to only update the attack map for your own attacks discovered by evacuation of the from-square in the previous ply, (usually none at all), clear the bit of the piece that was captured in this ply in the 'presence mask', and start looping through the opponent's piece list top-down to find a piece that is attacked. If you don't find any such piece before you reach the futility limit, you are done with the node, undo the partial map update for the discovery of your own attacks, (usually none), and return a fail low. Only if you do encounter a non-futile victim with an attack on it you would have to search its capture, and have to update the attack map of the opponent before you do that. (Which does involve adding new and removing old attacks of the moved piece, and discovering his moves over the previous from-square.) This update is then undone only after you searched all non-futile captures (or encountered a beta cutoff). The work for updating the attacks of the moved piece would thus be shared by all daughter nodes. (Where this piece may move again.)

My experience with a practical case: my Tenjiku-Shogi mailbox engine reaches ~500knps. The only serious competitor is a bitboard program, and it reaches ~5knps. Of course the situation for Tenjiku is a bit more favorable than for Xiangqi, due to the larger board size and number of pieces (and particularly bad for bitboards), but the fact remains that I still reach a speed on a 16x16 board with 78 pieces that would be typical for a 'naive' mailbox engine on an 8x8 board with 16 pieces.

[Edit]

Note that you can avoid looping through the piece list in most cases where there isn't any capture worth searching, by incrementally keeping track of the most-valuable attacked piece (MVV). If that is below the futility limit, you can only have non-futile captures if these were created by the preceding opponent's move, i.e. the opponent moved a soft-pinned piece, which you now can punish. This should become apparent when you discover your moves over the from-square (which again only involves any action if you had any extendable attacks to the from-square). So for the occasional discovered attack on a piece more valuable than the old MVV and above the futility limit you woul have to update the MVV. (If the update would stay below the futility limit you can fail low immediately.)

If the higest attacked piece was above the futility limit, you can use it as a starting point for looping through the piece list; it might not be attacked anymore because the previous ply resolved the attack. E.g. by capturing the attacker, or moving the piece away. In which case you would have to loop through the piece list anyway to find out what is now the MVV.
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Chinese Chess

Post by hgm »

In a more explicit way:

The majority of tree nodes are leaves. The common case for such leaves would execute the code

Code: Select all

QuiescenceSearch()
{
  int scoreGap = alpha - MARGIN - pstEval;
  if(scoreGap < 0) {                        // rarely
    ... // stand-pat stuff
  }
  victimSet = attacked[xstm];              // pre-existing capture targets
  if((map[stm][toSqr] & ATTACKS) == 0) {   // preceding move captured (optically) unprotected piece
    victimSet &= ~piece2bit[piece];        // withdrawn piece is now (tentatively) safe from capture
  }
  present[stm] &= ~piece2attack[victim];   // take note of captured piece
  int blocks = present[stm] & BLOCKBITS;   // bits in attack map indicating discoverable moves
  worthy = futilityLimit[scoreGap >> 4];   // set of non-futile victims
  if((victimSet & worthy) == 0) {          // all attacks that existed before preceding move are futile
    int todo = map[stm][fromSqr] & blocks; // moves that were discovered by preceding half-move
    if(!todo) return alpha;                // moved piece did not block anything
    ... // discover the blocked moves, updating the attack map at their new targets
    }
  ... // test again if there are non-futile captures, aborting the node after undoing the upate if not
  }
  ... // there are non-futile captures to search; finish update of attack map
}
That is almost nothing, compared to complete from-scratch move generation that would otherwise have to be done to conclude there are no eligible captures. The more complex part of the codes at the ... would not have to be executed in the common case. Occasionally there is a single move to discover, and only rarely more than a single move.

In most cases the discovered move(s) would not produce a (non-futile) attack, in which case this (minor) part of the attack-map update would have to be undone, and we can abort the node anyway. Note that pieces that move along the ray of a slider attack on them will correct the initial clearing of the attack on them by discovering the latter, finding themselves as the 'new target'.

In attacked[color] each bit would correspond to a piece, and be set if the attack-map element of the square where the piece is on indicates no captures. (It might indicate blocks; these are masked away with ATTACKS.) If we assign the bits here in such a way that they are extracted in the order high-to-low piece value, we can use it to speed up looping over the piece list while generating captures: we can use LSB extraction to get the next attacked piece directly, skipping any non-attacked pieces.

[Edit]

On second tought it might be a bad idea to try to keep track of the complete set of attacked pieces: it would not be easy to remove pieces from the list that are no longer attacked because a former attacker got captured. Even if we kept track of the set of pieces attacked by each individual piece, we could not just remove the pieces in that set from the 'attacked' set, as there could still be other attacks on them.

So it would probably be better to just keep track of the highest attacked piece, and loop through the list starting there to find the new highest attacked piece when an attacker of the old one dies.
User avatar
hgm
Posts: 27796
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Chinese Chess

Post by hgm »

Poor man's SEE

To do better than MVV/LVA capture ordering, some HxL captures would have to be postponed or pruned. Because a full SEE is rather expensive, and deep SEE is unreliable anyway, we use the following simplified rule to decide whether a capture should be postponed compared to strict MVV/LVA order:
* Never postpone capture of unprotected pieces.
* Capture of multiply protected pieces with higher-valued pieces is postponed.
* Capture of singly attacked protected pieces with higher-valued pieces is postponed.
* Capture of multiply-attacked singly protected pieces with a piece higher than both victim and protector is postponed.
To apply these rules the pieces are organized in 'value groups' {K}, {R}, {C, H} and {E, A, P}. The rules (and in particular the last one) make use of the fact that trading one piece of a higher group for two in a lower group is not really better than an equal trade in Xiangqi. (Unlike Chess, where R for B+N would be significantly positive.) And because 2-for-1 trades loses the initiative, making any gain afterwards will not be possible. Capture of unprotected pieces of course also loses the initiative, but there at least you gain the full piece.

Since captures will be generated from the set of attackers stored in the attack map, we want to mask out the attacks we want to postpone before extracting the indiviual captures. Otherwise we would have to test each inividual capture for the postponement condition, and then store those that have to be postponed somewhere in a list, to later search moves from that list. It will be more efficient to just do a second round of extractions for the postponed moves, in which these are then searched immediately, without having to store them anywhere. Not only in total, but also because you then never have to do it, because one of the good captures already produced a cutoff earlier.

For now we assume that we have masks available that we use to remove pinned pieces, and pieces captured on preceding plies from the sets of attackers tabulated in the attack map. (The latter removes the need to delete the attacks by captured pieces from the attack map.) So when we run through the opponent's piece list over the possible capture victims, we will for each of those have two sets of their active attackers available, one for the true attackers, and the other for the pieces that protect it. Depending on the latter, attacks by pieces above a certain value would have to be excluded from the former, before they are extracted an searched.

The basic scheme for capture generation is then as follows, in a loop over the opponent piece list:

Code: Select all

  // for each non-futile victim (highest to lowest value)
  f.toSqr = location[victim];
  attackers = map[stm][f.toSqr] & myActive;
  if(attackers) {                                    // victim can be captured
    int protectors = map[xstm][f.toSqr] & hisActive; // set of pieces protecting it
    int n = LowBit(attackers);
    int lowestAtt = (1<<n);                          // set with LVA as only element
    int todo;
    if(!protectors) todo = attackers;                // any capture is good on unprotected victim
    else {                                           // victim is protected
      int mask = lowerEq[victim];                    // set of pieces lower or equal than victim
      if(attackers > lowestAtt) {                    // victim is multiply attacked
        m = LowBit(protectors);
        lowestProt = 1<<m;                           // set with lowest-valued protector
        if(protectors == lowestProt) {               // only singly protected
          mask |= lowerEq[m];                        // also good if attacker <= protector
        }
      }
      todo = attackers & mask;                       // remove the bad or dubious captures
    }
    postpone = attackers ^ todo;                     // remember what we removed

    while(todo) {                       // process (good) captures that remain
      int n = LowBit(todo);             // extract in LVA order
      int piece = bit2piece[n];
      f.fromSqr = location[piece];
      if(SearchMove(&f)) goto cutoff;   // and search the move (f.fromSqr, f.toSqr)
      todo -= 1<<n;                     // one more done
    }

    if(postpone) {                  
      badAtt[bptr] = postpone;          // stack any postponed (bad) captures
      badVictim[bptr++] = f.toSqr;
    }
  }
The test for being singly attacked or protected is one here by extracting the lowest attacker/protector, constructing the set with only that one, and comparing this with the actual attackers set. Any postponed attacks are saved here, so they can be searched later. They are mostly bad captures, however, so perhaps we would like to prune them in QS. But as this qualification does not take X-rays into account, this is a bit tricky. Note that X-ray attacks are a special case of discovered attacks, where the moved piece happens to capture the target of the discovered attack. And that other (SEE-wise) bad captures with the discovering piece can just as easily turn into good ones by following up (after recapture) with the discovered capture. Anyway, if we don't want to prune the postponed captures, we could follow the loop over victims by

Code: Select all

  for(i=0; i<bptr; i++) { // for all postponed (bad) captures
    int todo = badAtt[bptr];            // retrieve yet untreated attackers
    f.toSqr = badVictim[bptr];
    while(todo) {                       // process (good) captures that remain
      int n = LowBit(todo);             // extract in LVA order
      int piece = bit2piece[n];
      f.fromSqr = location[piece];
      if(SearchMove(&f)) goto cutoff;   // and search the move (f.fromSqr, f.toSqr)
      todo -= 1<<n;                     // one more done
    }
  }
This code just shows the basic idea; in practice we want to splice the attacks on two equal victims into one 'todo' set, to get MVV/LVA ordering of the good captures.