Check out Grant Acedrex, our featured variant for April, 2024.


[ Help | Earliest Comments | Latest Comments ]
[ List All Subjects of Discussion | Create New Subject of Discussion ]
[ List Earliest Comments Only For Pages | Games | Rated Pages | Rated Games | Subjects of Discussion ]

Comments/Ratings for a Single Item

Earlier Reverse Order LaterLatest
GAME code table-driven move generator[Subject Thread] [Add Response]
H. G. Muller wrote on Sat, Jul 25, 2020 12:42 PM UTC:

It seemed best to start an independent comment thread for this. The goal of this project is to develop GAME code that can be almost universally used for rule enforcement in Game Courier presets. The rules that will be enforced (piece moves, possible promotions, initial piece shuffling) will be encoded in some tables, which are then initialized in the Pre-Game section. These CV-specific tables can then be hand-coded, but also generated automatically through the Play-Test Applet, and copy-pasted in the Pre-Game section from there.

To not get ahead of ourselves, the project will be divided into several phases, where each phase would correspond to a functional preset, which will progressively allow less and less violation of the game rules. Phase 0 will test the basic method of handling the moves. To remain in full control over vetting and performing the moves, the preset will be created with the checkbox "do not include moves in GAME code" ticked. This means the preset would not do anything at all with entered moves, unless the Post-Game code tells it what to do.

The minimum requirement is thus to let the Post-Game code order execution of the entered move(s). This will already be done the way the finished project would do it: all code for handling a move will go into a subroutine HandleMove, and the only code in the Post-Move sections will be calling this subroutine:

gosub HandleMove false;

and

gosub HandleMove true;

where the Boolean parameter indicates whether the black player is making the move. The Pre-Game code for phase 0 will define the subroutine as:

setsystem maxmove 4;
ban commands;
sub HandleMove player:
  eval join "MOVE: " thismove;
endsub;

This will order Game Courier to do what it would have done automatically if we hadn't ticked the checkbox: perform the move exactly as entered, "no questions asked". For now the input can consist of any combination of up to 4 moves, promotions, drops, etc.

In phase 1 the input move will be tested for syntactical correctness, before we allow its execution.


H. G. Muller wrote on Sat, Jul 25, 2020 05:38 PM UTC:

Phase 0 seems to work now. I create a preset Sandbox to try it out. (due to a faulty d key on my laptop the settings are saved in the file sanbox.) I notice this completely bypasses any checking on the entered moves; one can also move pieces of the opponent. When parsing the input move this thus also has to be checked.

Next is phase 1: testing whether the entered move could in principle be a valid move for a chess variant of the type that can be handled by the Play-Test Applet. These are simple moves (possibly performing replacement capture at their destination), moves that alter the piece type (promotions), moves that as a side effect remove one or two pieces from other squares, or make a piece appear on another square.

In Game Courier this can require entering a sequence of moves for one turn. We will enforce the convention that the first of these will always be a normal displacement of a piece of the player whose turn it is, like "P e7-e8". This can then be followed by putting a piece on the destination square on the first move ("Q-e8") to indicate a promotion. Such a ' drop move' should also be used to make a captured piece appear elsewhere. If a piece is captured as a side efect ('locust capture'), this can be indicated by dropping an empty square on top of it ("@-d4"). All the additional actions can thus be indicated through extra moves with the drop syntax; they just differ in where or what they drop.

We will allow one exception to this: a locust capture can also be entered by first making the capture in the normal chess way, moving to the location of the victim, and moving from there to the destination in a second 'normal' move. So additional board moves will be allowed, provided that they start where a previous board move ended. The intermediate square must then have been occupied, and the combination of board moves will have exactly the same effect as when that intermediate square had been mentioned in the drop of an empty square, to effect locust capture.

A tentative routine for parsing the input move is given below. Its purpose is to set the variables origin, dest, moved, promo, suicide, freedrop and dropped, in accordance with the entered move in thismove. The first three of those describe the mandatory first move, and will always be set when the move is accepted. The next three describe the optional side efects of promoting, removing a piece, adding a piece. When the move will not have the corresponding side effect, they will be set to false. The value of dropped will only be defined when a piece was added, and in that case holds the mentioned piece type.

sub ParseMove player:
  my i j mvs parts sqrs;
  set many suicide false freedrop false promo false;
  set mvs explode chr 59 thismove;                   // split at semicolons
  set parts explode ws trim elem 0 #mvs;             // split first move at space
  set i count #parts;
  if > #i 2:                                         // too many parts
    die "move must be piece ID plus board step";
  endif;
  set sqrs explode chr 45 trim elem dec #i #parts;   // split last part at hyphen
  if != 2 count #sqrs:                               // must give 2 squares
    die "board step does not mention two squares";
  endif;
  set origin elem 0 #sqrs;                           // first is origin
  set dest elem 1 #sqrs;                             // second is dest
  if not onboard origin or not onboard dest:         // check if these are valid
    die "the board isn't that large";
  endif;
  set moved space origin;                            // look what we move
  if != moved elem 0 #parts and == 1 #i:             // input move mentioned another piece
    die "there is no " elem 0 parts " on " origin;
  endif;
  if not cond #player islower moved isupper moved:   // it is not our piece
    die "you have no piece on " origin;
  endif;
  set j 1;                                           // prepare to do side effects
  while < #j count #mvs:                             // for each of those
    set parts explode chr 45 trim elem #j #mvs;      // split them at hyphen
    if != 2 count #parts:                            // must give 2 parts
      die "malformed move " elem #j #mvs;
    elseif not onboard elem 1 #parts:                // second part must be valid square
      die "second leg goes off board";
    elseif onboard elem 0 #parts:                    // additional move for locust capture
      if != dest elem 0 #parts:                      // must start from destination of first move
        die "you can only continue with same piece";
      endif;
      if #suicide:                                   // for now only one side efect
        die "cannot have more than one locust victim";
      endif;
      set suicide dest;                              // old dest was actually locust square
      set dest elem 1 #parts;                        // and this is true destination
    elseif == dest elem 1 #parts:                    // drop on dest: promotion
      set promo elem 0 #parts;
    elseif == @ elem 0 #parts:                       // drop of empty: locust square
      if #suicide:
        die "cannot have more than one locust victim";
      endif;
      set suicide elem 1 #parts;
    else:                                            // drop of piece: unload
      if #freedrop:
        die "cannot kick more than one piece";
      endif;
      set dropped elem 0 #parts;
      set freedrop elem 1 #parts;
    endif;
    inc #j;                                          // go on with next side effect
  loop;
endsub;

 

 


H. G. Muller wrote on Sat, Jul 25, 2020 10:26 PM UTC:

Well, that did not work quite as planned. For one, the # does not seem to work in subroutines; I had to replace it by var everywhere. What caused me the worst headache was the afct that origin and dest seem to be protected variables; everything worked fine upto the point where sqrs became an array (f3 f5), but after set origin elem 0 var sqrs; the value of origen was not the expected f3, but @ ! After I change the names to ori, desti and mover the preset worked again. At least to the point where it did not die, and executed the entered moves. I don't manage to print the variables to see if they obtained the expected values; the page where I printed them flashed away too fast to read. I suppose I would have to intentionally let it die to see it.

sub ParseMove player:
  my i j mvs parts sqrs;
  set many suicide false freedrop false promo false;
  set mvs explode chr 59 thismove;                    // split at semicolons
  set parts explode ws trim elem 0 var mvs;           // split first move at space
  set i count var parts;
  if > var i 2:                                       // too many parts
    die "move must be piece ID plus board step";
  endif;
  set sqrs explode chr 45 trim elem dec var i var parts; // split last part at hyphen
  if != 2 count var sqrs:                                // must give 2 squares
    die "board step does not mention two squares";
  endif;
  set ori elem 0 var sqrs;                            // first is origin
  set desti elem 1 var sqrs;                          // second is dest
  if not onboard var ori or not onboard var desti:    // check if these are valid
    die "the board isn't that large";
  endif;
  set mover space var ori;                            // look what we move
  if != var mover elem 0 var parts and == 1 var i:    // input move mentioned another piece
    die "there is no " elem 0 var parts " on " var ori;
  endif;
  if not cond var player islower var mover isupper var mover: // it is not our piece
    die "you have no piece on " var ori;
  endif;
  set j 1;                                            // prepare to do side effects
  do while < var j count var mvs:                     // for each of those
    set parts explode chr 45 trim elem var j var mvs; // split them at hyphen
    if != 2 count var parts:                          // must give 2 parts
      die "malformed move " elem var j var mvs;
    elseif not onboard elem 1 var parts:              // second part must be valid square
      die "second leg goes off board";
    elseif onboard elem 0 var parts:                  // additional move for locust capture
      if != desti elem 0 var parts:                   // must start from destination of first move
        die "you can only continue with same piece";
      endif;
      if var suicide:                                 // for now only one side efect
        die "cannot have more than one locust victim";
      endif;
      set suicide var desti;                          // old dest was actually locust square
      set dest elem 1 var
    elseif == desti elem 1 var parts:                 // drop on dest: promotion
      set promo elem 0 var parts;
    elseif == @ elem 0 var parts:                     // drop of empty: locust square
      if #suicide:
        die "cannot have more than one locust victim";
      endif;
      set suicide elem 1 var parts;
    else:                                             // drop of piece: unload
      if var freedrop:
        die "cannot kick more than one piece";
      endif;
      set dropped elem 0 var parts;
      set freedrop elem 1 var parts;
    endif;
    inc j;                                            // go on with next side effect
  loop;
endsub;

H. G. Muller wrote on Sun, Jul 26, 2020 06:25 AM UTC:

The link to the Sandbox preset stopped working ( https://www.chessvariants.com/play/pbm/play.php?game%3DSandbox%26settings%3Dsanbox ). When I click it, the "busy loading" indicator of the browser is on for a few seconds, and when it goes off the page is completely blank.

Any idea what could cause this? I have no idea how to progress now.


dax00 wrote on Sun, Jul 26, 2020 08:34 AM UTC in reply to H. G. Muller from 06:25 AM:

You left out a "d" in the last "sandbox". It works fine.


H. G. Muller wrote on Sun, Jul 26, 2020 09:43 AM UTC in reply to dax00 from 08:34 AM:

You are right! With d it works fine. Which is really weird, because the d was already missing when I typed the settings-file name when creating the preset. To access the preset yesterday I therefore had to use the link without a d, and this is the link that is in one of the earlier comments in this thread. Someone must have renamed the file.

Anyway, thanks. Now I can continue.


H. G. Muller wrote on Sun, Jul 26, 2020 05:31 PM UTC:

It seems I was too hasty: it is not fine at all. With 'sandbox' as name for the settings file it does not give me a blank page, but what it gives looks nothing like what I had made yesterday. It is another position with other graphics, all of the code has disappeared... It seems more that when the name of the settings file and the game are the same, and no file with that name exists, it just defaults to orthodox chess without any rule enforcement.

So the issue remains: the settings file 'sanbox', where I saved all yesterday's work on, doesn't exist anymore, or cannot be retrieved or some other reason.

The question is: how can this happen?


🕸Fergus Duniho wrote on Sun, Jul 26, 2020 08:15 PM UTC in reply to H. G. Muller from 05:31 PM:

One line of your code in sanbox.php was missing a semicolon at the end. This caused it to not load. I added the semicolon by directly editing the file, and it loads now.


H. G. Muller wrote on Sun, Jul 26, 2020 08:44 PM UTC in reply to Fergus Duniho from 08:15 PM:

Ah, thanks! Is there a way to edit a preset without Game Courier first attempting to execute the code? It is really inconvenient if an accidental deletion could make the preset go in hiding this way...

I still don't understand how it could have happened in this case; the preset seemed to work fine until I stopped working on it. I wouldn't make any significant changes without immediately testing them. Perhaps something went wrong when I deleted a print statement I had used for debugging, and I accidentally deleted something else as well.

There is something else I don't understand either. I have two subroutines, HandleMove and ParseMove, each with formal parameter 'player'. I try to make one call the other:

sub HandleMove player:
  gosub ParseMove var player;
  eval join "MOVE: " thismove;
endsub;

But when I print 'player' in ParseMove (for debugging purposes) it prints 'var'!? Can the actual parameters of a subroutine call not be expressions like 'var player'?

 


🕸Fergus Duniho wrote on Sun, Jul 26, 2020 09:56 PM UTC in reply to H. G. Muller from 08:44 PM:

Is there a way to edit a preset without Game Courier first attempting to execute the code?

Yes, include "&submit=Edit" in the query string.

Can the actual parameters of a subroutine call not be expressions like 'var player'?

They cannot. Only some commands evaluate expressions, and gosub does not. Use #player instead.


H. G. Muller wrote on Mon, Jul 27, 2020 09:24 AM UTC in reply to Fergus Duniho from Sun Jul 26 09:56 PM:

Yes, include "&submit=Edit" in the query string.

Great, that will be very helpful.

They cannot. Only some commands evaluate expressions, and gosub does not. Use #player instead.

I first had used #player , but then I noticed that the preset only allowed entry of moves for white, even when it said it was black's turn to move. Black moves were always rejected (by my ParseMove code) with the complaint that he wasn't moving his own piece. I figured that this was because #player was statically dereferenced in the Pre-Game section (where the ParseMove code is inserted), where white is on move.

[Edit] OK, I have got it working. But I don't understand why! The problem could be traced to the following:

printr player;
print join "var player=" var player;
print join "#player=" #player;

When the formal parameter player is passed as false in the subroutine call, the output is

false
var player=
#player=

as expected, as GAME code's default representation for true and false as the result o an expression appears to be 1 and the empty string, respectively. However, when the subroutine is called with arument true, it prints

true
var player=
#player=1

The value of var player is apparently not dependent on the actual value stored in player. Why is that? I understood that var is supposed to be more accurate than # in this respect.

Another issue: I get the error message solved

BANNED INPUT: P e3-e5; e5-e6 on turn 1:

You may not enter more than 4 move primitives in a move in this game.

Indeed I did setsystem maxmove 4. But how does it figure this input is more than 4 move 'primitives'? I count only 2 moves... Is each piece or coordinate a 'move primitive', and should I really set maxmove to 9 to allow one move, one promotion, one suicide and one freedrop?

This was due to continuing the loop over the partial moves after exploding thismove continued indefinitely, because there was no var in front of the loop index in the while condition. It is just that the error message is a bit confusing.


🕸Fergus Duniho wrote on Mon, Jul 27, 2020 05:14 PM UTC in reply to H. G. Muller from 09:24 AM:

I don't have your old code to look at. In your current code, I do not see anyplace where you set a value for player. Since you were trying to use it with var or as #player, I assume it was a user variable. Also, $player is not an available system variable.


H. G. Muller wrote on Mon, Jul 27, 2020 05:28 PM UTC in reply to Fergus Duniho from 05:14 PM:

player was (and still is) the formal parameter of the ParseMove subroutine. It received its value in the gosub call indirectly, through gosub ParseMove true; , which then does a gosub HandleMove #player; The debug prints I mentioned were inserted in ParseMove (because a cond var player ... expression appeared to give the wrong result).

The only thing I changed to get the current code is that it now does cond #player ..., and I deleted the debug print / printr.


🕸Fergus Duniho wrote on Mon, Jul 27, 2020 06:18 PM UTC in reply to H. G. Muller from 09:24 AM:

The problem was that player was an undocumented operator, and var player would not see player as the string "player". I'm not sure of what use it could be, since it returns the userid of the current player, which is useful only in email games. I don't think it would have any application for test games someone plays against himself. Because of that, and because it is undocumented anyway, I got rid of it.


H. G. Muller wrote on Mon, Jul 27, 2020 06:34 PM UTC in reply to Fergus Duniho from 06:18 PM:

Ah, so this was just a very unlucky choice of variable naming. I was afraid there was something I completely misunderstood about the workings of var and #.

Another question: I had planned to use a flag for every board square, indicating whether a move had gone to that square. By doing setflag dest; on every move. Then we could be sure that the piece that is on it (if any) must have moved before. This because XBetza in principle could define initial moves on any piece.

I am starting to doubt now whether this can be used. To use the flags for testing whether a piece at a given location #to is virgin, I would have to test the flag with the name held in to. Would ?#to work for this?


🕸Fergus Duniho wrote on Mon, Jul 27, 2020 07:45 PM UTC in reply to H. G. Muller from 06:34 PM:

To use the flags for testing whether a piece at a given location #to is virgin, I would have to test the flag with the name held in to. Would ?#to work for this?

Not presently. I need to separate variable evaluation into its own function and make it recursive, or at least mimic recursion with a loop. I'll look into that.


H. G. Muller wrote on Mon, Jul 27, 2020 08:46 PM UTC in reply to Fergus Duniho from 07:45 PM:

Wouldn't flag #to do what I want? Or is the flag operator just meant for things like flag a8 ?

An alternative would be not to use flags for this at all, but just an array. I woul have to set up my own mapping from square names to array entries by something like set index + * 100 rank #to file #to;


🕸Fergus Duniho wrote on Mon, Jul 27, 2020 10:10 PM UTC in reply to H. G. Muller from 08:46 PM:

Yes, flag #to should do what you want. ?#to should also now work, since I have placed variable evaluation into its own recursive function.


H. G. Muller wrote on Tue, Jul 28, 2020 05:17 AM UTC in reply to Fergus Duniho from Mon Jul 27 10:10 PM:

OK, great. That means all all issues encountered so far are solved now, and we are ready to continue. First a summary of where we are, because I was too optimistic in declaring phase 0 finished. The code shown befor turned out only to work for simple moves; Game Courier does not like it when you try to evaluate a composit move by putting "MOVE:" in front of it, and will complain the move is malformed in that case. So it is necessary to explode composit moves into an array mvs of simple moves, and have Game Courier perform these one by one in a loop. This exploding of moves had to be done anyway for parsing the move, but it is now done before we call ParseMove, and mvs is made a global variable, so that it will also be available inside ParseMove:

sub HandleMove player:
  set mvs explode chr 59 thismove; // split at semicolons
  gosub ParseMove #player;         // syntax check and interpretation
  set k 0;
  do while < var k count var mvs:  // for all legs
    eval join "MOVE: " trim elem var k var mvs; // apply the move
    inc k;
  loop;
endsub;

For phase 0 the call to Parsemove would not yet be there. The tested and debugged code for phase 1 is:

sub ParseMove player:
  my i j parts sqrs;
  set many suicide false freedrop false promo false;
  set parts explode ws trim elem 0 var mvs;            // split first move at space
  set i count var parts;
  if > var i 2:                                        // too many parts
    die "move must be piece ID plus board step";
  endif;
  set sqrs explode chr 45 trim elem dec var i var parts; // split last part at hyphen
  if != 2 count var sqrs:                                // must give 2 squares
    die "board step does not mention two squares";
  endif;
  set ori elem 0 var sqrs;                             // first is origin
  set desti elem 1 var sqrs;                           // second is dest
  if not onboard var ori or not onboard var desti:     // check if these are valid
    die "the board isn't that large";
  endif;
  set mover space var ori;                             // look what we move
  if != var mover elem 0 var parts and == 1 var i:     // input move mentioned another piece
    die "there is another piece there";
  endif;
  if not cond #player islower var mover isupper var mover: // it is not our piece
    die "no such piece there";
  endif;
  set j 1;                                             // prepare to do side effects
  do while < var j count var mvs:                      // for each of those
    set parts explode chr 45 trim elem var j var mvs;  // split them at hyphen
    if != 2 count var parts:                           // must give 2 parts
      die "malformed move " elem var j var mvs;
    elseif not onboard elem 1 var parts:               // second part must be valid square
      die "second leg goes off board";
    elseif onboard elem 0 var parts:                   // additional move for locust capture
      if != var desti elem 0 var parts:                // must start from destination of first move
        die "you can only continue with same piece";
      endif;
      if #suicide:                                     // for now only one side efect
        die "cannot have more than one locust victim";
      endif;
      set suicide var desti;                           // old dest was actually locust square
      set desti elem 1 var parts;
    elseif == var desti elem 1 var parts:              // drop on dest: promotion
      set promo elem 0 var parts;
    elseif == @ elem 0 var parts:                      // drop of empty: locust square
      if #suicide:
        die "cannot have more than one locust victim";
      endif;
      set suicide elem 1 var parts;
    else:                                              // drop of piece: unload
      if #freedrop:
        die "cannot kick more than one piece";
      endif;
      set dropped elem 0 var parts;
      set freedrop elem 1 var parts;
    endif;
    inc j;                                             // go on with next side effect
  loop;
endsub;

This extracts values for ori, desti, mover, promo, suicide, freedrop and dropped from the move. The first three from the (mandatory) first move. The others describe the optionally following moves of the turn, and are set to 0 by default. These values are not yet used in phase 1; the only observable effect is that the preset will now enforce turn order, as it no longer accepts moving of opponent pieces. It still accepts capture of own pieces, as XBetza can allow that. So this must be left for move generator to check out.

Next step is to take care of implied side effects: it should be possible (even mandatory) to enter castling and e.p. capture as a simple move. In phase 2 we will do this for castling. Phase 3 will then address e.p. capture, which is more complex, because it also has to address the creation of e.p. rights.

 


H. G. Muller wrote on Tue, Jul 28, 2020 06:52 AM UTC:

Move legality testing will be based on a move generator. This generates the pseuo-legal moves that are possible in the given position, always being fully aware of any side effects these moves might have. These moves are then compared to the input move, and if there is a match we know the input move was pseuo-legal. Normally we consider a move a match only if all the parameters ori, desti, promo, suicide, freedrop and dropped of the input move are equal to their counterparts orisqr, destsqr, locustsqr, dropsqr and unload of the generated move. But for moves with implied side effects we only match ori, desti and promo (the latter should be 0), and accept what the move generator specifies as side effects. E.g. for castling the locustsqr would be at the Rook to make it disappear, and the dropsqr would indicate where we must drop a Rook to make it reappear.

To make this process efficient, we generate only moves of the piece that according to the input is going to move. For moves that were already tested for legality before (now played for setting up the current position) we even take a short-cut on that: we only generate the moves with implied side effects, to calculate and perform those. The subroutine GenMoves will thus have three parameters: the piece type and start location from which its moves should be generated, and a Boolean that indicates whether we want all moves of the piece, or just those with implied side effects. In the latter case most pieces would of course have no moves at all; in orthodox Chess only the KIng (castling) and Pawns (e.p.) have moves with side efects.

sub GenMoves piece sqr all:            // generates moves for 'piece' on 'sqr' in current position
                                       // 'all' specifies whether we get only moves with side effects
  my index legcount;
  set side islower #piece;             // remember which side we are generating for
  set index fn #piece #all;            // function returns start of piece description in legdefs table
  set hit false;
  do:
    set legcount elem #index #legdefs; // the first element specifies the number of legs
    verify #legcount;                  // a 0 sentinel ends the list of moves
    inc index;                         // 'reads away' the leg count
    gosub NextLeg #legcount #index #sqr #sqr 0 0 0;
    set index + #index * 4 #legcount;  // skip to the next set of legs (= move)
  loop until #hit;
endsub;

The move descriptions come from the table legdefs, and where in this table the description of the given piece type starts is given by a function with the name of the piece type. This function has an additional argument, indicating whether we want all moves or just those with implied side effects. The latter will be found behind the former in the legdefs table, so that we can just start later in the table to get only those. Each move description will start with a number indicating how many legs the move has (where each leg is a leap or a ride). Each leg is then described by 4 numbers: range, sideway and vertical step, and mode. We call the subroutine NextLeg to interpret these. To get to the next move of a piece the table index then has to be incremented by 1 plus 4 times the number of legs. A dummy move with 0 legs indicates we are done with this piece, and ends the loop by terminating the subroutine though verify. A global variable hit can cause the loop to terminate early; there is usually no need to go on generating moves after we found the one we were looking for.

The routine NextLeg does the real work. It calls itself recursively in case there is more than one leg, to do the follow-up legs. Its first parameter controls the depth of this recursion, the next specifies where in legdefs to find the four numbers that describe the leg. Then follow a number of squares: the start square of the move, and the start square of the leg (which for the first leg are of course the same), then the squares for the side effects (starting at the default 0) locustsqr and dropsqr. Finally there is a parameter with which we can request an exact length of a ride when it is non-zero.

We will not go into the details of NextLeg now. When it succeeds in constructing the move according to the description in legdefs, it will eventually call a routine GotMove, passing it the parameters that describe the move and all its side effects. Because we will want to use the move generator for various purposes (e.g. testing whether a given move is pseuo-legal, regeneration of implied side effects, testing whether we can capture a royal piece, making a table of legal moves...), a global variable task will specify what this routine must do. For now we will only have 'task 1', regeneration of the implied side effects.

sub GotMove orisqr destsqr locustsqr dropsqr unload implied:
  switch #task:
    case 1:
      verify == #orisqr #ori and == #destsqr #desti;
      if not #implied: // explicitly specified side effects must also match
        verify == #locustsqr #suicide;
        verify == #dropsqr #freedrop;
        verify == #unload #dropped or not #dropsqr;
     else:            // no side effects must be specified when they are implied
        verify not #suicide and not #freedrop;
        if #locustsqr:
          capture #locustsqr;
        endif;
        set implieddrop #dropsqr;
      endif;
      set hit true;    // we found a matching pseudo-legal move
  endswitch;
endsub;

The move generator always specifies the side effects, and whether these are implied. If they are explicit, they will be automatically applied by Game Courier's execution of the input move. If they were implied, we perform any locust capture, and remember whether we should drop a piece somewhere, so we can do that later.

Why not immediately do all implied side efects? Well, we cannot always make the freedrop of a castling Rook before Game Courier has made the input move, as in some castlings the Rook then would clobber the King. So we must apply the freedrop after feeding the move to Game Courier.

sub HandleMove player:
  set mvs explode chr 59 thismove; // split at semicolons
  gosub ParseMove #player;         // syntax check and interpretation
  set task 1;
  set implieddrop 0;
  gosub GenMoves #mover #ori 0;    // only moves with implied side effects
  set k 0;
  do while < var k count var mvs:  // for all legs
    eval join "MOVE: " trim elem var k var mvs; // apply the move
    inc k;
  loop;
  if #implieddrop:                 // implied freedrop
    add #dropped #implieddrop;
  endif;
endsub;

Note that this code doesn't worry about promotions yet.


H. G. Muller wrote on Tue, Jul 28, 2020 09:00 AM UTC:

Now that the infrastructure for legality testing has been defined, we can pay attention to the heart of the move generator, the subroutine NextLeg. The first parameter of this indicate how many legs we will have yet to go. It is decremented every time the function recursively calls itself, and when it hits 0 we have completed the move, and can pass it to GotMove. Otherwise we have to retrieve the leg description from legdefs, and see if it is possible to move that way in the given position. The basic step of the leg is given, and the range specifies how many of those step we can maximally take. If this is 1, we are dealing with a leap. If it is larger, but the specified mode compells us to finish on a square with a particular occopant (friend or foe), there is still at most one way to do that. Only when the mode allows going to empty squares can we have more than one possibility to realize the leg, and even then the iso parameter can force the destination. But if we do have a 'free ride' we have to loop over all possible leg destinations, and if more legs follow, continue with the next leg from each of those. This happens for hook movers.

For phase 2 we only want to do castlings. These are encoded as two-leg moves, the first leg specifying a slide of the King that must end on the Rook. This is similar to a 'riendly capture' sliding leg, but still a special case in many ways: it must end in a specific location, the piece there must not have moved. So we have dedicated code for that, triggered by a special mode, only used for castling. Instead of following it by a new general leg through a recursive call, this dedicated code interprets the leg that follows by itself; the only info used from it is how many steps the King must move. Where the Rook will then reappear is implied by that.

sub NextLeg togo legindex startsqr cursqr locustsqr dropsqr iso:
  my range dx dy mode to tosqrs;
  // retrieve leg description (next 4 elements in 'legdefs')
  set range elem #legindex #legdefs;
  set dx elem + 1 #legindex #legdefs;
  set dy elem + 2 #legindex #legdefs;
  set mode elem + 3 #legindex #legdefs;
  verify not flag #startsqr or not & 64 #mode; // 64 = initial
  set tosqrs ray #startsqr #dx #dy;
  set r count #tosqrs;
  verify >= #range #r;
  set to elem dec #r #tosqrs;             // last square of ride
  set stopper space #to;                  // what is on it
  set side islower space #startsqr;       // whether we are black
  set fratricide cond #side islower #stopper isupper #stopper;
  if #fratricide:
    if & 8 #mode: // 8 = first leg of castling
      verify match #to #partners;         // ends at castling partner
      verify not flag #to;                // partner is virgin
      set locustsqr #to;                  // order its removal
      set to where #startsqr elem + 5 #legindex #legdefs 0;
      set dropsqr where #to - 0 #dx #dy;  // make it appear on other side of King
      gosub GotMove #startsqr #to #locustsqr #dropsqr #stopper 1;
    endif;
  endif;
endsub;

This code tests for virginity in two places. First, in the general code, it tests the moving piece when the mode indicates we are dealing with an initial move (which for conventional  castling will  be the case). Then in the dedicated it tests whether the castling partner is virgin. For these tests it uses flags with the name of the board squares. This assumes that in the HandleMove routine, ater each move, there will be a setflag #desti; to indicate the destination square no longer contains its original occupant. If this flag is not set, together with the fact that the square is occupied, it is a guarantee that the original piece is still on the square without having moved.


H. G. Muller wrote on Tue, Jul 28, 2020 07:22 PM UTC:

I stumbled again on a mystery. I define this large array, and some code to print what is in it:

set legdefs
(0
1  1  0  1     1
1  1  1  1     2
1  1 -1  1     2
1  1  0  2   16577 // pawn(1)
1  1  1  1     4
1  1 -1  1     4
0
1  1  0 -1     1
1  1 -1 -1     2
1  1  1 -1     2
1  1  0 -2   16577 // pawn(32)
1  1 -1 -1     4
1  1  1 -1     4
0
//... snip ...
2 99 -1 -1    16
  99 -1 -1     2
2 99 -1  1    16
  99 -1  1     2
0);

set k 0;
do while < #k 100:
  print join #k join ". " elem #k #legdefs;
  inc k;
loop;
die "stop";

But what it prints is this:

...
20. 16577
21. 1
22. 1
23. 1
24. 1
25. 4
26. 1
27. 1
28. -1
29. 1
30. 4
31. 1
32. 1
33. 0
34. -1
...

It seems the 0 at location 31 did not get into the array. That does not seem kosher to me...


🕸Fergus Duniho wrote on Tue, Jul 28, 2020 08:04 PM UTC in reply to H. G. Muller from 07:22 PM:

It seems the 0 at location 31 did not get into the array. That does not seem kosher to me...

That's now fixed. In some early preprocessing of the lines, it checked for an empty line, which would return true when the line equaled "0". I changed it to check the length of the string, and it now works.


H. G. Muller wrote on Tue, Jul 28, 2020 09:09 PM UTC:

OK, that helps. My code now seems to work! When I move the King 3 steps to the left or right it automatically moves the Rook to the other side. Unless I move the King or the Rook back and forth first to destroy their virginity; then it just moves the King because it doesn't match any pseuo-legal castling. So that completes phase 2.

Next phase is to automate e.p. capture. This will again require a lot of new infra-structure, for handling the e.p. rights. In XBetza moves can be specified as rights-generating in several ways: a lame leap can generate e.p. rights on the square where it could be blocked, and slides can generate e.p. rights on every square they pass through. In the diagram it proved convenient to also use 'royal e.p. capture': e.p. rights created by moving a royal piece can be consumed by any capture, not just by pieces that have been defined as e.p.-capable. This then prevents the royal to pass through check. Castlings also create e.p. rights, even on the square the King starts from, to prevent castling out of check.

So moves can in principle create e.p. rights on a lot of squares, along a complex trajectory; XBetza would allow you to define a Griffon as e.p.-capturable piece. It seems thus best to create an array epsqrs, which contains all the squares with rights created in the previous move, and use match to test whether the current destiation is one of those.

This brings me to the following question:

How can I create an array in GAME code? I know I can write the elements in parentheses, but to use that there must be a fixed number of elements, all known in advance. But what if I want to do it in a loop, to add the elements one by one? Is there a way to create an empty array? Can I push something to a so-far unused variable to create an array of one element? Can I push something to a scalar variable to make it an array of two elements? Can I use setelem on a so-far unused variable?


🕸Fergus Duniho wrote on Tue, Jul 28, 2020 10:41 PM UTC in reply to H. G. Muller from 09:09 PM:

But what if I want to do it in a loop, to add the elements one by one?

I recently added better support for arrays. See this comment for details:

https://www.chessvariants.com/index/listcomments.php?id=39825

Is there a way to create an empty array?

Set it to an empty pair of parentheses, like so:

set ra ();

Can I push something to a so-far unused variable to create an array of one element?

Yes, there is a push command.

Can I push something to a scalar variable to make it an array of two elements?

I hadn't thought of that. You can now. The previous behavior was to hang the program without a helpful error message. So, changing that is not going to break anything.

Can I use setelem on a so-far unused variable?

Yes. You can now also use set. See the link above.


25 comments displayed

Earlier Reverse Order LaterLatest

Permalink to the exact comments currently displayed.