My first attempt at a reference guide is now at
http://hgm.nubati.net/Betza.html .
for Chess-variant authors
Moderators: hgm, Rebel, chrisw
-
- Posts: 27793
- Joined: Fri Mar 10, 2006 10:06 am
- Location: Amsterdam
- Full name: H G Muller
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: for Chess-variant authors
One of the complications I'm having in implementing a parser for this in Sjaak (a different problem from sending the piece moves to XBoard of course) is in handling the multiple ways in which piece moves can be encoded that are all equivalent, but don't naturally map that way.
Consider a two-square orthogonal slider. As I understand it, this can be encoded in Betza notation in a number of ways:
What I find particularly gross is that W2 means the same thing as R2; I noticed you didn't include the latter use in your writeup (or I missed it), which is a good thing.
I guess part of the problem here comes from the fact that Betza notation wasn't designed as something that would be easy for computers to interpret, but for humans. As such, it's a bit fuzzy.
Consider a two-square orthogonal slider. As I understand it, this can be encoded in Betza notation in a number of ways:
- W2
- WnD
- R2 (!)
What I find particularly gross is that W2 means the same thing as R2; I noticed you didn't include the latter use in your writeup (or I missed it), which is a good thing.
I guess part of the problem here comes from the fact that Betza notation wasn't designed as something that would be easy for computers to interpret, but for humans. As such, it's a bit fuzzy.
-
- Posts: 27793
- Joined: Fri Mar 10, 2006 10:06 am
- Location: Amsterdam
- Full name: H G Muller
Re: for Chess-variant authors
What I did in the parser I posted here is consider BRQ as a synonyms for FWK, but with another default for the range (0 = infinite, while the genuine atoms use 1). An actually written range then overrules this, making Wn and Rn fully equivalent already during the low-level parsing.
As for the W2 and WnD descriptions: aren't you too ambitions here? WnD is inferior Betza notation (using more atoms), so I don't think there is any reason to worry if it also compiles to inferior code. In Fairy-Max' internal encoding there are the same options: using a normal W-slider, (which can do anything on its first step), that turns into a W-leaper after one step (W2). Or, less efficiently, use a W-leaper and a separate W-slider without permission to do anything, which turns into a W-leaper that can do everything after the first step (nD). In the latter case you would examine the W board-squares twice, and waste your time on the nD step even after you found out that the W hit an occupied square in that direction.
Well, so be it. That is what you get for feeding it crappy Betza notation. The two-atom mechanism obviously has to be allowed to handle cases like WnH, for which there is no single-atom representation. Or, more common, for the double-push of the Pawn, WinD, where you cannot combine because the nD is only an initial move, while the W is always possible. It could also be written WiW2, but that is also ugly, because it contains the same move twice, for virgin pieces.
Writing a compiler does not necessarily involve writing an optimizer.
The 'again' operator that I introduced for describing the multi-leg Shogi pieces creates similar dilemmas. The most compact way to describe a Lion is pmcaK, saying it can hop, move or capture with its first King move, and then again do an unrestricted King move. This describes all direct jumps as hops or moves over empty squares, and single K steps in a quirky way as two-leg moves that took a detour. The problem is that the pmaK part alone describes 64 moves, while there are really only 25, so many duplicats. This form is poorly suited for generating moves in an engine. The notation KNADcaKmabK would be much more suitable for the latter: all 24 direct jumps are generated seapartely by KNAD, then caK requires the first leg to be a capture, and needs never go into the second when it isn't. And mabK describes the null move in 8 different ways. (Which is the only remaining overhead, but hard to avoid, as we might actually have to test all neighboring squares to know whether it can null move. The only thing you miss by literally following the prescription is that you might take a cutoff after discovering the first null move. This is a generic problem with null moves, as different pieces could each have null move, which is then also the same move.)
I think it is perfectly acceptable to require from a user that he would write 'good Betza' here, avoiding unnecessary duplicat moves, but otherwise minimizing the number of atoms.
As for the W2 and WnD descriptions: aren't you too ambitions here? WnD is inferior Betza notation (using more atoms), so I don't think there is any reason to worry if it also compiles to inferior code. In Fairy-Max' internal encoding there are the same options: using a normal W-slider, (which can do anything on its first step), that turns into a W-leaper after one step (W2). Or, less efficiently, use a W-leaper and a separate W-slider without permission to do anything, which turns into a W-leaper that can do everything after the first step (nD). In the latter case you would examine the W board-squares twice, and waste your time on the nD step even after you found out that the W hit an occupied square in that direction.
Well, so be it. That is what you get for feeding it crappy Betza notation. The two-atom mechanism obviously has to be allowed to handle cases like WnH, for which there is no single-atom representation. Or, more common, for the double-push of the Pawn, WinD, where you cannot combine because the nD is only an initial move, while the W is always possible. It could also be written WiW2, but that is also ugly, because it contains the same move twice, for virgin pieces.
Writing a compiler does not necessarily involve writing an optimizer.
The 'again' operator that I introduced for describing the multi-leg Shogi pieces creates similar dilemmas. The most compact way to describe a Lion is pmcaK, saying it can hop, move or capture with its first King move, and then again do an unrestricted King move. This describes all direct jumps as hops or moves over empty squares, and single K steps in a quirky way as two-leg moves that took a detour. The problem is that the pmaK part alone describes 64 moves, while there are really only 25, so many duplicats. This form is poorly suited for generating moves in an engine. The notation KNADcaKmabK would be much more suitable for the latter: all 24 direct jumps are generated seapartely by KNAD, then caK requires the first leg to be a capture, and needs never go into the second when it isn't. And mabK describes the null move in 8 different ways. (Which is the only remaining overhead, but hard to avoid, as we might actually have to test all neighboring squares to know whether it can null move. The only thing you miss by literally following the prescription is that you might take a cutoff after discovering the first null move. This is a generic problem with null moves, as different pieces could each have null move, which is then also the same move.)
I think it is perfectly acceptable to require from a user that he would write 'good Betza' here, avoiding unnecessary duplicat moves, but otherwise minimizing the number of atoms.
-
- Posts: 27793
- Joined: Fri Mar 10, 2006 10:06 am
- Location: Amsterdam
- Full name: H G Muller
Re: for Chess-variant authors
I made some more extensions, so that it is now also possible to describe sliders that make 45-degree turns. The trick was to define the g-modifier as a general flavor-changing operator in non-final legs, turning leapers into riders and vice versa, but also upgrading 4-fold movers to 8-fold K or Q, so you can then also select non-perpendicular directions from them in the follow-up leg. This does not conflict with the old Betza meaning of 'g' as Grasshopper, as there it could be used on sliders, to change them into leapers after the hop, by resetting their range to 1. I just defined what it would do to other atoms in addition to that. And I introduced a new modifier 'y', which does the same thing, but not on hopping, but on an empty square, to describe trajectories that bend 'spontaneously'.
I think I am going to do a Chess variant with bifurcation pieces in Fairy-Max now: Rooks and Bishops that bounce 45-degrees in either direction from the first obstacle in their path: mRgabyabsR and mBgabyabsB.
I think I am going to do a Chess variant with bifurcation pieces in Fairy-Max now: Rooks and Bishops that bounce 45-degrees in either direction from the first obstacle in their path: mRgabyabsR and mBgabyabsB.
Code: Select all
// configurable move generation from Betza notation sent by engine.
// Released in the public domain by H.G. Muller
// Some notes about two-leg moves: move generation works in two modes, depending on whether a 'kill-
// square has been set: without one is generates all moves, and a global int legNr flags in bits 0 and 1
// if the move has 1 or 2 legs. Only the marking of squares makes use of this info, by only marking
// target squares of leg 1 (rejecting null move). A dummy move with MoveType 'FirstLeg' to the relay square
// is generated, so a cyan marker can be put there, and other functions can ignore such a move. When the
// user selects this square, it becomes the kill-square. Once a kill-square is set, only 2-leg moves are
// generated that use that square as relay, plus 1-leg moves, so the 1-leg move that goes to the kill-square
// can be marked during 2nd-leg entry to terminate the move there. For judging the pseudo-legality of the
// 2nd leg, the from-square has to be considered empty, although the moving piece is still on it.
Boolean pieceDefs;
// alphabet "abcdefghijklmnopqrstuvwxyz"
char symmetry[] = "FBNW.FFW.NKN.NW.QR....W..N";
char xStep[] = "2110.130.102.10.00....0..2";
char yStep[] = "2132.133.313.20.11....1..3";
char dirType[] = "01000104000200000260050000";
char upgrade[] = "AKCD.QGH.JQL.NO.KK....Q..Z";
// alphabet "a b c d e f g h i j k l m n o p q r s t u v w x y z "
int dirs1[] = { 0,0x3C,0,0,0,0xC3,0,0, 0,0,0,0xF0,0,0,0,0,0,0x0F,0 ,0,0,0 ,0,0,0,0 };
int dirs2[] = { 0,0x18,0,0,0,0x81,0,0xFF,0,0,0,0x60,0,0,0,0,0,0x06,0x66,0,0,0x99,0,0,0,0 };
int dirs3[] = { 0,0x38,0,0,0,0x83,0,0xFF,0,0,0,0xE0,0,0,0,0,0,0x0E,0xEE,0,0,0xBB,0,0,0,0 };
int dirs4[] = { 0,0x10,0,0,0,0x01,0,0xFF,0,0,0,0x40,0,0,0,0,0,0x04,0x44,0,0,0x11,0,0,0,0 };
int rot[][4] = { // rotation matrices for each direction
{ 1, 0, 0, 1 },
{ 0, 1, 1, 0 },
{ 0, 1,-1, 0 },
{ 1, 0, 0,-1 },
{-1, 0, 0,-1 },
{ 0,-1,-1, 0 },
{ 0,-1, 1, 0 },
{-1, 0, 0, 1 }
};
void
OK (Board board, int flags, ChessMove kind, int rf, int ff, int rt, int ft, VOIDSTAR cl)
{
(*(int*)cl)++;
}
void
MovesFromString (Board board, int flags, int f, int r, int tx, int ty, int angle, char *desc, MoveCallback cb, VOIDSTAR cl)
{
char buf[80], *p = desc;
int mine, his, dir, bit, occup, i;
if(flags & F_WHITE_ON_MOVE) his = 2, mine = 1; else his = 1, mine = 2;
while(*p) { // more moves to go
int expo = 1, dx, dy, x, y, mode, dirSet, ds2, retry=0, initial=0, jump=1, skip = 0;
char *cont = NULL;
if(*p == 'i') initial = 1, desc = ++p;
while(islower(*p)) p++; // skip prefixes
if(!isupper(*p)) return; // syntax error: no atom
dx = xStep[*p-'A'] - '0';// step vector of atom
dy = yStep[*p-'A'] - '0';
dirSet = 0; // build direction set based on atom symmetry
switch(symmetry[*p-'A']) {
case 'B': expo = 0; // bishop, slide
case 'F': // diagonal atom (degenerate 4-fold)
if(tx < 0) { // for continuation legs relative directions are orthogonal!
while(islower(*desc) && (i = dirType[*desc-'a']) != '0') {
int b = dirs1[*desc-'a']; // use wide version
if( islower(desc[1]) &&
((i | dirType[desc[1]-'a']) & 3) == 3) { // combinable (perpendicular dim)
b = dirs1[*desc-'a'] & dirs1[desc[1]-'a']; // intersect wide & perp wide
desc += 2;
} else desc++;
dirSet |= b;
}
dirSet &= 0xAA; if(!dirSet) dirSet = 0xAA;
break;
}
case 'R': expo = 0; // rook, slide
case 'W': // orthogonal atom (non-deg 4-fold)
while(islower(*desc) && (dirType[*desc-'a'] & ~4) != '0') dirSet |= dirs2[*desc++-'a'];
dirSet &= 0x55; if(!dirSet) dirSet = 0x55;
dirSet = (dirSet << angle | dirSet >> 8-angle) & 255; // re-orient direction system
break;
case 'N': // oblique atom (degenerate 8-fold)
if(tx < 0) { // for continuation legs relative directions are non-degenerate!
while(islower(*desc) && (i = dirType[*desc-'a']) != '0') {
int b = dirs2[*desc-'a']; // when alone, use narrow version
if(desc[1] == 'h') b = dirs1[*desc-'a'], desc += 2; // dirs1 is wide version
else if(*desc == desc[1] || islower(desc[1]) && i < '4'
&& ((i | dirType[desc[1]-'a']) & 3) == 3) { // combinable (perpendicular dim or same)
b = dirs1[*desc-'a'] & dirs2[desc[1]-'a']; // intersect wide & perp narrow
desc += 2;
} else desc++;
dirSet |= b;
}
if(!dirSet) dirSet = 0xFF;
break;
}
case 'Q': expo = 0; // queen, slide
case 'K': // non-deg (pseudo) 8-fold
while(islower(*desc) && (i = dirType[*desc-'a']) != '0') {
int b = dirs4[*desc-'a']; // when alone, use narrow version
if(desc[1] == *desc) desc++; // doubling forces alone
else if(islower(desc[1]) && i < '4'
&& ((i | dirType[desc[1]-'a']) & 3) == 3) { // combinable (perpendicular dim or same)
b = dirs3[*desc-'a'] & dirs3[desc[1]-'a']; // intersect wide & perp wide
desc += 2;
} else desc++;
dirSet |= b;
}
if(!dirSet) dirSet = 0xFF;
dirSet = (dirSet << angle | dirSet >> 8-angle) & 255; // re-orient direction system
ds2 = dirSet & 0xAA; // extract diagonal directions
if(dirSet &= 0x55) // start with orthogonal moves, if present
retry = 1; // and schedule the diagonal moves for later
else dx = 1, dirSet = ds2; // if no orthogonal directions, do diagonal immediately
break; // should not have direction indicators
default: return; // syntax error: invalid atom
}
if(mine == 2 && tx < 0) dirSet = dirSet >> 4 | dirSet << 4 & 255; // invert black moves
mode = 0; // build mode mask
if(*desc == 'm') mode |= 4, desc++;
if(*desc == 'c') mode |= his, desc++;
if(*desc == 'd') mode |= mine, desc++;
if(*desc == 'e') mode |= 8, desc++;
if(*desc == 'p') mode |= 32, desc++;
if(*desc == 'g') mode |= 64, desc++;
if(*desc == 'o') mode |= 128, desc++;
if(*desc == 'y') mode |= 512, desc++;
if(*desc == 'n') jump = 0, desc++;
while(*desc == 'j') jump++, desc++;
if(*desc == 'a') cont = ++desc;
if(isdigit(*++p)) expo = atoi(p++); // read exponent
if(expo > 9) p++; // allow double-digit
desc = p; // this is start of next move
if(initial && (board[r][f] != initialPosition[r][f] ||
r == 0 && board[TOUCHED_W] & 1<<f ||
r == BOARD_HEIGHT-1 && board[TOUCHED_B] & 1<<f ) ) continue;
if(expo > 1 && dx == 0 && dy == 0) { // castling indicated by O + number
mode |= 16; dy = 1;
}
if(!cont) {
if(!(mode & 15)) mode = his + 4; // no mode spec, use default = mc
} else {
if(mode & 32) mode ^= 256 + 32; // in non-final legs 'p' means 'pass through'
if(mode & 64 + 512) {
mode |= 256; // and 'g' too, but converts leaper <-> slider
if(mode & 512) mode ^= 0x304; // and 'y' is m-like 'g'
strncpy(buf, cont, 80); cont = buf; // copy next leg(s), so we can modify
while(islower(*cont)) cont++; // skip to atom
*cont = upgrade[*cont-'A']; // replace atom, BRQ <-> FWK
if(expo == 1) *++cont = '0'; // turn other leapers into riders
*++cont = '\0'; // make sure any old range is stripped off
cont = buf; // use modified string for continuation leg
}
if(!(mode & 0x30F)) mode = his + 0x104; // and default = mcp
}
if(dy == 1) skip = jump - 1, jump = 1; // on W & F atoms 'j' = skip first square
do {
for(dir=0, bit=1; dir<8; dir++, bit += bit) { // loop over directions
int i = expo, j = skip, hop = mode, vx, vy;
if(!(bit & dirSet)) continue; // does not move in this direction
if(dy != 1) j = 0; //
vx = dx*rot[dir][0] + dy*rot[dir][1]; // rotate step vector
vy = dx*rot[dir][2] + dy*rot[dir][3];
if(tx < 0) x = f, y = r; // start square
else x = tx, y = ty; // from previous to-square if continuation
do { // traverse ray
x += vx; y += vy; // step to next square
if(y < 0 || y >= BOARD_HEIGHT) break; // vertically off-board: always done
if(x < BOARD_LEFT) { if(mode & 128) x += BOARD_RGHT - BOARD_LEFT; else break; }
if(x >= BOARD_RGHT) { if(mode & 128) x -= BOARD_RGHT - BOARD_LEFT; else break; }
if(j) { j--; continue; } // skip irrespective of occupation
if(!jump && board[y - vy + vy/2][x - vx + vx/2] != EmptySquare) break; // blocked
if(jump > 1 && board[y - vy + vy/2][x - vx + vx/2] == EmptySquare) break; // no hop
if(x == f && y == r) occup = 4; else // start square counts as empty
if(board[y][x] < BlackPawn) occup = 0x101; else
if(board[y][x] < EmptySquare) occup = 0x102; else
occup = 4;
if(cont) { // non-final leg
if(occup & mode) { // valid intermediate square, do continuation
if(occup & mode & 0x104) // no side effects, merge legs to one move
MovesFromString(board, flags, f, r, x, y, dir, cont, cb, cl);
if(occup & mode & 3 && (killX < 0 || killX == x && killY == y)) { // destructive first leg
int cnt = 0;
MovesFromString(board, flags, f, r, x, y, dir, cont, &OK, &cnt); // count possible continuations
if(cnt) { // and if there are
if(killX < 0) cb(board, flags, FirstLeg, r, f, y, x, cl); // then generate their first leg
legNr <<= 1;
MovesFromString(board, flags, f, r, x, y, dir, cont, cb, cl);
legNr >>= 1;
}
}
}
if(occup != 4) break; // occupied squares always terminate the leg
continue;
}
if(hop & 32+64) { if(occup != 4) { if(hop & 64 && i != 1) i = 2; hop &= 31; } continue; } // hopper
if(mode & 8 && y == board[EP_RANK] && occup == 4 && board[EP_FILE] == x) { // to e.p. square
cb(board, flags, mine == 1 ? WhiteCapturesEnPassant : BlackCapturesEnPassant, r, f, y, x, cl);
}
if(mode & 16) { // castling
i = 2; // kludge to elongate move indefinitely
if(occup == 4) continue; // skip empty squares
if(x == BOARD_LEFT && board[y][x] == initialPosition[y][x]) // reached initial corner piece
cb(board, flags, mine == 1 ? WhiteQueenSideCastle : BlackQueenSideCastle, r, f, y, f - expo, cl);
if(x == BOARD_RGHT-1 && board[y][x] == initialPosition[y][x])
cb(board, flags, mine == 1 ? WhiteKingSideCastle : BlackKingSideCastle, r, f, y, f + expo, cl);
break;
}
if(occup & mode) cb(board, flags, NormalMove, r, f, y, x, cl); // allowed, generate
if(occup != 4) break; // not valid transit square
} while(--i);
}
dx = dy = 1; dirSet = ds2; // prepare for diagonal moves of K,Q
} while(retry-- && ds2); // and start doing them
if(tx >= 0) break; // don't do other atoms in continuation legs
}
} // next atom
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: for Chess-variant authors
True, but what is "good" will depend on how the engine works internally, and that's not something a user could know (and it'll be different for different engines as well).hgm wrote:Or, more common, for the double-push of the Pawn, WinD, where you cannot combine because the nD is only an initial move, while the W is always possible. It could also be written WiW2, but that is also ugly, because it contains the same move twice, for virgin pieces.
Writing a compiler does not necessarily involve writing an optimizer.
Case in point, WiW2 is actually what I would have to translate WinD into, since (at least in the development version) the "special" (initial) move replaces the normal move in Sjaak's move generator.
Again though, what is "good Betza" is not so obvious and depends on the engine. Avoiding duplicate moves looks like an obviously good thing, but if the move generator actually pulls possible destination squares from a bitboard that was initialised based on the string it doesn't matter - and in fact it might be better to define (say) BW as BK, since I can then just re-use the movement table for the king instead of making up a new set for a piece that is not in the game.I think it is perfectly acceptable to require from a user that he would write 'good Betza' here, avoiding unnecessary duplicat moves, but otherwise minimizing the number of atoms.
As I said, it's going to be very engine-specific what the "best" notation is, which isn't necessarily bad (we don't really want to tell each other how we should write our engines), but it'd be nice if at least the obvious alternatives would work equally well...
-
- Posts: 27793
- Joined: Fri Mar 10, 2006 10:06 am
- Location: Amsterdam
- Full name: H G Muller
Re: for Chess-variant authors
OK, this is true. But some things seem obviously bad, whether the engine is bit-board based or mailbox. Like putting squares where you can go to and where another move can be blocked in different atoms, which is exactly what WnD does. If a set of moves can be combined to a trajectory that is traversed in a slider-like fashion, like Wn, this should certainly be done.
And I wonder how much can be gained by combining leaper tables that accidentally are the same for different pieces. That sounds like a kind of micro-optimization that you cannot expect from a general table-driven system.
And I wonder how much can be gained by combining leaper tables that accidentally are the same for different pieces. That sounds like a kind of micro-optimization that you cannot expect from a general table-driven system.
-
- Posts: 27793
- Joined: Fri Mar 10, 2006 10:06 am
- Location: Amsterdam
- Full name: H G Muller
Re: for Chess-variant authors
I update the Betza reference guide with the newly conceived modifiers. To do the bifurcator pieces I had to add a 'y' for spontaneous range toggling from one leg to another, and a 't' to specify friendly-hops only. But after that it worked without a hitch. (Well, Fairy-Max also had to be upgraded to support 'early hopping' and friends-only hopping.)
I wonder if I still should build in a provision for 'long leapers', with ranges larger than the Betza system defines atoms for. One way would be to allow (x,y) to indicate such atoms. OTOH, with the 'again' operator you could describe them as two steps with a 'universal non-destructive' intermediate. E.g. a (2,4) leaper can be described as mpafN: two knight steps which go through empty or hop in between. In principle any move can be made through a succession of King steps with such all-purpose intermediates. This is of course quite awkward, but unlikely to be needed much.
I wonder if I still should build in a provision for 'long leapers', with ranges larger than the Betza system defines atoms for. One way would be to allow (x,y) to indicate such atoms. OTOH, with the 'again' operator you could describe them as two steps with a 'universal non-destructive' intermediate. E.g. a (2,4) leaper can be described as mpafN: two knight steps which go through empty or hop in between. In principle any move can be made through a succession of King steps with such all-purpose intermediates. This is of course quite awkward, but unlikely to be needed much.
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: for Chess-variant authors
It's checked when the piece descriptions are loaded into the move generator, so it's fast. The test is crude, the code will not try to generate the minimal (or optimal) set of tables it can, just re-use when possible.hgm wrote: And I wonder how much can be gained by combining leaper tables that accidentally are the same for different pieces. That sounds like a kind of micro-optimization that you cannot expect from a general table-driven system.
It makes a small, but measurable difference in speed for the move generator.
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: for Chess-variant authors
Question: is there a way for me to not specify how one particular piece moves?
Of course that means the XBoard will not be able to do legality testing (at least not while those pieces are on the board), but it would be quite useful if there are some pieces that I can't easily tell it how they move could be skipped.
Of course that means the XBoard will not be able to do legality testing (at least not while those pieces are on the board), but it would be quite useful if there are some pieces that I can't easily tell it how they move could be skipped.
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: for Chess-variant authors
I haven't yet, but I'm going to add (x, y) for long leapers to Sjaak's Betza compiler. Adding more atoms isn't going to be scalable, and such pieces are not very common or usable, even on large boards.hgm wrote:I wonder if I still should build in a provision for 'long leapers', with ranges larger than the Betza system defines atoms for. One way would be to allow (x,y) to indicate such atoms. OTOH, with the 'again' operator you could describe them as two steps with a 'universal non-destructive' intermediate. E.g. a (2,4) leaper can be described as mpafN: two knight steps which go through empty or hop in between. In principle any move can be made through a succession of King steps with such all-purpose intermediates. This is of course quite awkward, but unlikely to be needed much.
Implementing "again" is going to be rather awkward with the way Sjaak's move generator works, so I'm leaving that out for now. Multi-leg moves aren't really what it was designed to handle gracefully anyway (except for lame leapers).