Move trajectories radiate out from a piece. If at every square in the trajectory the choice is only between continue and stop, life is simple, and the trajectory can be stored sequentially in an array (the moveTable). But some pieces will have trajectories that conditionally branch: depending on the occupancy of a square that they reach they could continue in one direction or another. There can even be trajectories that branch unconditionally: a 'Hook Mover', which can make two perpendicular Rook moves over empty squares, in addition to one normal one, can continue in three different directions on any empty square. So basically trajectories are trees, and a tree-walk is necessary to extract the individual moves from them.
The case with conditional branches would specify a single continuation for each of the three occupancies (empty, friend, foe). One of these could be termination rather than continuation. This could be specified by the number of steps in the moveTable we have to advance to get at the next cell of the trajectory. This never needs to be very large, so it could be stored in a byte. Usually it would have value 0 (terminate) or 1 (continue with next); an alternative branch would have to be stored beyond the end of the current one. (If that is out of range because the principal continuation would need more than 256 cells, you could always interleave them, and then step through those in steps of 2 instead of 1.)
Pieces that only branch conditionally follow a single path for any given board occupancy although that path can be different for different occupancies. E.g. consider a piece that starts off like a Rook until it arrives at an occupied square, and then turns 45-degrees left to continue as a Bishop. The squares it visits in this second leg of the move have a different location w.r.t. the piece depending on where it makes the bend. So either we must tabulate different trajectories for this second leg for every point along the first leg where it could bend (in which case we can include the locations relative to the piece as a fixed value), or we would have to specify the squares on the trajectory not relative to the piece, but relative to where the leg started. Then we would have to store only a single trajectory for the second leg in moveTable, which could save appreciable storage on a large board.
Now locations in the moveTable will be relative to something anyway, even if it is only relative to the piece. So it isn't really much extra trouble to let each entry in moveTable specify what its reference location is, e.g. through a pointer. For the first leg of the move (which for the majority of pieces would be the only leg) these would point to the location of the piece they belong to in the piece list. But we could have auxiliary location variables for indicating the start of continuation legs, to which these legs then refer. These would be updated dynamically, as the position on the board changes such that the total trajectory would bend on another square. E.g. if the obstacle that triggered the bending would move away, the path would no longer bend there. So the 2nd leg would have to be dissociated from the board cells it was associated with, the next downstream obstacle on the first leg would have to be identified, its location stored as reference for the new second leg, and finally the cells in that leg should be associated with the new board squares they go to (for as far as that leg now can continue on the board).
Now pieces are usually symmetric. Which means that when their path can bend to the left, it can also bent to the right. This makes it their path bifurcate. This could still use the same scheme, however, provided we allow two different continuation trajectories in the case of an occupied square, both indicating their location to the branching square. (And a single continuation at an empty square.)
If the second leg is colinear with the first there is no branching, and the second leg visits the same squares as the first would have. At first glance it seems wasteful to dissociate the second leg and then associate it further downstream (and associate some more cells of the first leg as well) in case the obstacle that caused the transition moves away. But the rules for what the move could do (e.g. capture and/or non-capture) could be different in the second leg from what it would be in the first, as would be the rules for continuation. (When you are already in a second leg, you won't be able to transition to it like you would be on the first, and a new obstacle would terminate the move.) To change all that information is likely more work than just associating and dissociating the cells containing the information, or at least not much less. And although pieces that need this frequently occur in chess variants, they still will form a small minority in the variants where they do appear. So there would be very little performance benefit in treating them differently from pieces that do change direction at a transition point.
The situation is different for hook movers and the like, where the trajectory branches in many different places from the first leg simultaneously. Then there is no way to avoid that all trajectories after the bending squares have to be separately tabulated in moveTable. In a sense they are not even second legs; they can refer their square locations relative to the piece. The first leg is just one huge tree. Consider a Hook Mover (making two perpendicular Rook moves) on a 19x19 board. (Yes, there is a game that has that: Maka Dai Dai Shogi.) For each of the 4 directions a move could start in it would have 18 cells in the straight-line trajectory, and from each of these cells there are two perpendicular trajectories, each also 18 cells long. So in tyotal 37x18 = 306 entries in moveTable, for each of the 4 (initial) move directions. Is this outside the range of a byte-size indication for how far down-stream to continue? Not at all: all 36 perpendicular trajectories could be interleaved, so that they specify continuation 36 entries further in moveTable in an empty square (and termination in an occupied one), and all start within a distance 36 from the last cell in the primary part of the trajectory. As this is pretty much a 'worst case', it justifies use of 8-bit continuation 'pointers'.
So it seems we need the following:
Code: Select all
typedef struct {
int next, prev; // for doubly linked square-association lists
int leap; // relative location
int piece; // index of piece that makes this move
int reference; // index of location reference in (extended) piece list
char rights; // flags indicating for which square occupancy the move can end here
char n_empty; // number of continuations at empty square
char n_friend; // number of continuations at occupied square
char n_foe; // number of continuations at occupied square
char empty[4]; // offsets of 'empty' continuations
char occup[4]; // offsets of 'occupied' continuations
} MoveTarget;
MoveTarget moveTable[MAXPOTENTIALMOVES];
* I combined the continuations for friend and foe, as I have never seen a variant where the path of a piece would continue in a different way depending on whether it hopped over a friend or foe.
* It is common that you can only hop over friends or foes, but it could be indicated in the 'rights' flags whether this is the case.
* But I think it is better to have separate n_friend and n_foe, which both would refer to the same occup[] table when non-zero.
* The flags can also indicate whether the hopping is destructive or not (i.e. whether the hopped-over piece should be removed).
* I took up to 4 possible continuations for each occupancy; this covers the case of making both 45-degree and 135-degree turns.
* These MoveTarget cells use 32 bytes of storage