Check out Janggi (Korean Chess), our featured variant for December, 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 ]

Single Comment

GAME code table-driven move generator[Subject Thread] [Add Response]
H. G. Muller wrote on Sun, Aug 2, 2020 06:32 PM UTC in reply to Fergus Duniho from 04:23 PM:

Indeed it works now. And I changed the plans a little. As e.p. capture cannot be done without creating e.p. rights, and this creation is a side effect of regular leaps and rides, implementing generation of the latter is the logical next step. To get some idea of whether the code is working, it will be used for highlighting pseudo-legal moves. Because such highlighting has to be done from the Post-Game sections, we first have to fill those, with gosub GameEnd false; and gosub GameEnd true;  for white and black, respectively. As Game Courier wants to have the complete list of moves for all pieces in advance, we also need a routine GenAll that calls GenMove for all pieces of a given player (which is the opponent of the player passed to GameEnd). This is pretty much standard:

sub GenAll player:
  my from piece;
  set hit false;
  if #player:
    def friends onlylower;
  else:
    def friends onlyupper;
  endif;
  for (from piece) fn friends:
    gosub GenMoves #piece #from 1;
  next;
endsub;

For now the only task of the GameEnd routine is to use this  'all-moves' generator for filling the array of legal moves; a new task (2) is defined in GotMove to do a setlegal #orisqr #destsqr there. In a later stage this would only have to be done for moves that need explicit specification of side effects; we don't want to encourage the user to click the destination of such moves without typing the side effects! The routine GameEnd just specifies the task, and inverts the player.

sub GameEnd player:
  set task 2;
  set side not #player;
  gosub GenAll #side;
  drawn;
endsub;

That is all pretty straightforward. The real work is done by NextLeg, which is now expanded to:

sub NextLeg togo legindex startsqr cursqr locustsqr dropsqr iso:
  my range dx dy mode to tosqrs k len newindex hx hy;
  // retrieve leg description (next 4 elements in 'legdefs')
  set rng 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 #cursqr #dx #dy;         // squares along ray in this direction
  set r count #tosqrs;                    // view distance
  verify #r;                              // stop if nowhere to go
  // here we can shorten the range for non-jumping moves
  dec togo;                               // count this leg done
  // here we can set dropsqr for legs that unload
  set r min #rng #r;                      // farthest we can get (at least 1!)
  // here we can treat the 'iso' length request, and else
  if & 1 #mode:                           // leg includes free ride
    set k 0;
    do while < #k dec #r:                 // for non-final (and thus empty) squares
      set to elem #k #tosqrs;
      if not #togo:                       // no more legs follow
        gosub GotMove #startsqr #to #locustsqr #dropsqr 0 0;
      endif;
      inc k;
    loop;
  endif;
  // at this point only the move with exactly r steps can be left
  set len cond == 1 #rng #iso #r;           // if leg is slide, remember its length
  set to elem dec #r #tosqrs;               // target square, r steps away along ride
  set stopper space #to;                    // what is on it
  if == @ stopper:                          // target is empty
    verify & 1 #mode;                       // 1 = non-capture
  else:                                     // target is occupied
    // here we can handle hopping
    set side islower space #startsqr;       // whether we are black
    set fratricide cond #side islower #stopper isupper #stopper;
    if #fratricide:                         // target is friend
      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;
        return;
      endif;
      // here we can handle move induction
      verify & 4096 #mode;                  // 4096 = destroy (friendly capture)
    else:                                   // target is foe
      verify & 2 #mode:                     // 2 = capture
      // TODO: check-only moves
    endif;
  endif;
  if not #togo:
    gosub GotMove #startsqr #to #locustsqr #dropsqr #stopper 0;
  endif;
endsub;

The task is to generate all possible realizations of one leg of the move, where the leg is a ride that is specified in the legdefs table (at legindex) as its range, x-y step and 'mode'. The latter is a set of bit flags packed into an integer, which specifies what the leg can do (e.g. capture). For most legs the destination will be unique, e.g. because they are leaps, or because the mode requires a certain kind of occupant on the destination. And by definition a ride can only have one occupied square in it, at which it must terminate. The only case where a leg as several alternative destinations is thus when a true rider makes a non-capture move. In this case NextLeg has to loop over the destinations, and generate a move for each of them.

The algorithm of NextLeg roughly has three parts. First it determines the farthest square of the ride, taking into account the board size and occupation, the range of the leg, and whether a specific length was requested (to match that of an earlier leg). Then, if the mode allows non-captures, it loops over all squares up to that final square, which are guaranteed to be empty. Finally it determines if a move to the final square is possible, given the occupant and the mode. This can (in a non-final leg) still lead to two different realizations, one that captures the occupant, and one that uses it as a screen to hop over.

In phase 3 of the project we only handle simple moves, consisting of a single leg. For every realization of the leg NextLeg then calls GotMove to process the move. In multi-leg moves it might have to call itself recursively at those points, to attempt the remaining legs, but for now we can leave out these recursive calls. The only modes we handle is capture, non-capture and friendly capture. (The castling code of phase 2 goes into that latter section.) It is ignored whether a leap is lame, and the more exotic features of XBetza, such as move induction and unloading of victims are not implemented either. Hopping, locust capture, and bent riders are intrinsically two-leg phenomena, and won't work either.