Betza move compiler

Discussion of chess software programming and technical issues.

Moderators: hgm, Dann Corbit, Harvey Williamson

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

Betza move compiler

Post by hgm »

I have started a new fun project, which is to develop a JavaScript-powered web page that implements a board-game GUI, where the user can move the pieces over the board with the mouse. When grabbing a piece the squares where it can move to are highlighted. The initial purpose is just to provide an illustration of how pieces of various types can move, but in a later stage I might add an AI so that the page can suggest a move, when the user requests so by pressing a 'Hint' button. (Not too clever; probably just 2 ply search + QS.)

Preliminary trial: http://hgm.nubati.net/rules/applet.html

I am sure this has been done before (many times?), but what makes my implementation special is that it should be able to handle almost arbitrary Chess variants, through minimal configuration. So the board layout and piece overview are not hard-coded HTML elements, but are generated during page load by the JavaScript based on initialization of some of the JavaScript arrays specified by the page. E.g. dimensions of the board, number of piece type, piece IDs, names of the graphics file to be displayed for the pieces, etc.

One of the items to be initialized is the description of the piece move, as a text string in Betza notation. So one of the challenges was to develop JavaScript code for interpreting such strings, so they can be used to generate moves from. (Which then are used to highlight move targets of the picked-up piece considering the board position.) Now parsing Betza notation is a rather complex edeavor. For highlighting moves that is not a problem, as speed is not an issue there. But to keep the hope for an AI alive, a faster method should be used as parsing the text strings from scratch every time you want to generate moves.

So I separated the parsing of the text and the actual generation of moves, by implementing a (slow) Betza compiler (which generates some intermediate move code), and a (fast) interpreter of this intermediate code. Normally I just use a null-terminated list of board steps for each piece type, in a mailbox move generator for orthodox Chess. In general the same piece can have both slider and leaper moves, however (e.g. the Archbishop of Capablanca Chess), so you would have to indicate that on a per-move bases. And for handling the general case of 'divergent' pieces that capture differently from how they move would also require a per-move indicator whether the move is a capture or not. So in a variant engine like Fairy-Max each board step is accompanied by a 'flags' word which specifies what the piece can do with the given board step. To handle limited-range sliders the slider-vs-leaper flag can be generalized by specification of a range, where a range of 1 implies a leaper.

The intermediate code I have chosen for the Betza compiler elaborates on this design. The main problem to still be addressed is that of multi-leg moves: moves that do something (e.g. move to an empty square, hover above an occupied one, or capture a piece), and are then followed by more moving (in the same or another direction, possibly now able to do different things). So in general a move is not just a single (step, flags, range) combination, but a sequence of those, one for each leg of the move.

This raises the problem of failure to accomplish the move goal (e.g. a move that must move to an empty square hits upon an occupied one, or strays off board) in a leg that is not the final leg for that move. This then requires skipping of the remaining legs of that move, so that move generation can continue with the first (and perhaps only) leg of the next move of that piece (or finds the termination sentinel for the list of that piece).

The intermediate code handles this by including a 'skip' field for each leg, indicating how many following leg descriptors should be skipped in case of failure. (In case of success, e.g. a leg that must capture indeed finds an enemy on its destination square, it always continues with the next leg in the list.) So the intermediate move code consists of a list of (flags, step, range, skip) quadruples, where flags = 0 is used as a list terminator. I used one bit in the flags word to indicate if the leg is the final leg of the move, but I guess this also could have been deduced from the skip field being 0.

Each leg in the list specifies a unique direction. Betza notation in general specifies move in groups with the same step length, e.g. 'N' stands for all 8 Knight jumps. So the compilation process involves 'unrolling' such implied loops over directions. This can get complicated when a multi-leg description involves forking. E.g. a piece that can make two King steps in independent directions basically implies two nested loops over directions, one for each step, and thus would unroll into 64 different 'paths' of two legs each. A piece that can make 3 independent King steps even unrolls to 512 3-leg moves. That sounds like a lot of work for the move generator. Of course such a 'King cubed' is a rather complex piece; it might have to feel its moves through a maze of occupied squares. But the compiler can optimize this process by grouping those paths that start with the same step (64 of the 512), and set the skip field of the first leg to not just skip the remaining two legs of the path when it fails (e.g. because it bumps into a friend), but skip the following 63 paths as well. After all, they all start with the same leg that you just concluded will fail, and there is no need to try and fail in exactly the same way 63 more times. Similarly, failure of the second leg could skip the remaining leg plus the 7 following paths.

Apart from the directional unrolling, there is one other type of leg that implies looping, and would fork the path. This is a slider move with non-capture rights (i.e. allowed to end on an empty square) in the non-final leg of a move. Leaper moves with a given step always have a unique target, as do slider moves that must end on an occupied square. But when describing a Hook Mover (a Rook that can turn one 90-degree corner on any empty square), you woud specify it as a non-capture Rook move for its 1st leg, followed by a perpendicular Rook move. In general the first leg can now end on many different empty squares, which can all be used to alter direction, and consequently end up in different places.

In theory this implied loop over the empty squares along the ray could also be unrolled, by repeating it with ever larger strides for the 1st leg. The slider range is unlimited, but the board size would put a cap on it. This seemed a bit cumbersome, so I designed a different method. An otherwise invalid flags value of -1 makes the move-descriptor item not a true leg, but a 'loop directive'. This instructs the move generator to redo the follow-up legs of the sliding move. The way it works is that sliding non-final legs with non-capture rights immediately terminate in favor of the next leg when they hit the first square (and it was empty; otherwise they fail), and remember the square where they ended somewhere. A later loop directive then adds another board step to it (which it re-specified in its own step field) to that stored square number, and reloads it, while rewinding the 'current-leg pointer' to point to the original follow-up leg descriptor by subtracting the range field if that was successful. (I.e. when it stepped to an unoccupied square.) When the added step would not hit an empty square the loop directive fails.

This allows implementation of hook movers by just adding a simple loop directive behind the two-leg description, e.g.

Code: Select all

1. (noncapt,      (1,0),  inf, 2)     // 1st leg, slide in (1,0) direction to empty. Skip 2 (to 4.) if blocked
2. (noncapt|capt, (0,1),  inf, 0)     // 2nd leg, slide in (0,1) direction and capt/noncapt there. Always continue with 3.
3. (loop,         (1,0),   1,  0)     // take one (1,0) step from square reached by 1, and continue with 3-1 = 2.
4. (noncapt,      (1,0),  inf, 2)
5. (noncapt|capt, (0,-1), inf, 0)     // 2nd leg in perpendicular direction opposite from 2.
6. (loop,         (1,0),   1,  0)
7. ...
The skip field in (1) could actually have been 5 instead of 2, as when (1) fails in its first step, there is no reason to retry it as (4), and only step to (7) as a result of the certain fail of (4).

Finite-range slides in non-final non-capture legs can be simply unrolled by using successively larger strides for the 1st leg, e.g. for a max. range of 2:

Code: Select all

1. (noncapt,      (1,0),  1,  3)     // Only attempt one (1,0) step. skip to 4. if it is blocked
2. (noncapt|capt, (0,1), inf, 0)     // 2nd leg, slide in (0,1) direction and capt/noncapt there. Always continue with 3.
3. (noncapt,      (2,0),  1,  1)     // Directly jump over (2,0), as (1,0) is known to be empty
4. (noncapt|capt, (0,1), inf, 0)     // 2nd leg
5. ...
Such a move table allows pretty efficient interpretation in a simple move-generator loop.
Rein Halbersma
Posts: 741
Joined: Tue May 22, 2007 11:13 am

Re: Betza move compiler

Post by Rein Halbersma »

This looks very promising. Is the Javascript UI just a prototype that will be backported to the full WinBoard GUI?
User avatar
hgm
Posts: 27701
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Betza move compiler

Post by hgm »

For the moment it is more like forward porting. WinBoard 4.8.0 already has a Betza move generator, and the one in the JavaScript cannot do more (yet). In WinBoard it is a pure interpreter, which re-parses the text each time moves have to be generated for the piece, but in a GUI speed isn't really an issue. I might extend the JavaScript generator to handle moves with an arbitrary number of locust captures. In any case to handle two, as this is needed for the Lion Dog in the large Shogi variants.

For Draughts the number of captures is of course unlimited. A problem there is that the current flavor of Betza notation I am using (XBetza) cannot really describe that. A king in draughts would be mBpafmBpafmapafmBpafmapafmapafmB..., separately describing non-captures, single captures, double captures... There can be an arbitrary number of repetition of the prefix 'pafma', describing the move to the victim and its continuation in the same direction to an empty square. Perhaps I should introduce parentheses for this, like mBp(afmap)afmB, where the sub-string '(afmap)' means zero or more repetitions of 'afmap'.

For now I am thinking more about porting this to Fairy-Max, where speed really matters.
Rein Halbersma
Posts: 741
Joined: Tue May 22, 2007 11:13 am

Re: Betza move compiler

Post by Rein Halbersma »

OK, I see. So XBoard currently does not support Betza 2.0 where capture sequences can be expressed like fmF(fcF-mF)?

I must confess that Betza 2.0 is a bit byzantine and I haven't been able to figure out if it can support the many draughts variations around the world. E.g. do you know how to express

1) passing promotion: a man lands on the promotion line during a jump, promotes to a king, and continues to capture like a king until the jump sequence is completed (this is the Russian draughts variant).
2) delayed promotion: a man lands on the promotion line during a jump sequence, continues to capture like a man until he stops, after which the man is promotion to a king (this variant does not exist yet).
3) passing jump removal: a piece immediately removes a jumped piece (this is Turkish draughts, all other draughts variant only "mark" jumped piece and remove them after the jump sequence is completed)
4) direction reversal: a king (jumping like a bishop) after each intermediate jump, can jump in any of its directions, including where it came from (this is Thai draughts)
5) restricted intermediate and final landing squares: a king has to land immediately behind a jumped piece, but can slide towards the next jumpable piece (Thai draughts).
6) conditionally restricted final landing square: a king has to land immediately behind the last jumped piece, if and only if that final victim is a king. (this is the recently proposed Killer draughts variant, that has almost no draws in the endgame because 2 kings can beat 1 king).
User avatar
hgm
Posts: 27701
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Betza move compiler

Post by hgm »

Indeed, neither XBoard nor the JavaScript page support Betza 2.0 yet. Instead of the general chaining with hyphens it uses the 'a' (= 'again') modifier on the same atom. The obvious limitation of this is that you cannot switch atom betweenthe legs of a move, but in practice this is almost never needed. The indefinite repetition of a group of legs is solved in Betza 2.0 with parentheses. Adding parentheses to XBetza in the proposed way would similarly solve it. This would not only be important for Draughts, but also to describe pieces like the Mao-rider, which repeat lame non-linear leaps (that have to be described by multiple legs to specify the square where they can be blocked).

Turning back on itself would not be a problem in either Betza flavor. But on-the-fly promotions would be, because they break the board symmetry. In general Betza notation doesn't involve itself with promotion; after a promotion they are just considered different pieces, each with their own Betza description. Promotion during a move obviously wrecks that. Perhaps something can be done with the aid of 'limiters', of the kind like "this leg has to end on an empty square, which must be on last rank".

An ad-hoc solution to this in XBetza would be to have some symbol meaning "this move must end with promotion" (i.e. in the zone), which implies that during the same turn you continue to move with the promoted piece.

But Betza notation is not the only tool have an engine configure WinBoard. Much of the stuff you mention is so subtle that it would be better to have the engine handle it, and use the highlight protocol to indicate possible moves and promotions. WinBoard 4.8.0 does support the highlight protocol.