SAN test position

Discussion of chess software programming and technical issues.

Moderator: Ras

User avatar
Evert
Posts: 2929
Joined: Sat Jan 22, 2011 12:42 am
Location: NL

Re: SAN test position

Post by Evert »

matthewlai wrote: That is how I do it, too, but it's easy to forget checking for piece types for kings (castling) and pawns (which require special handling for promotions and always need file).

It's also easy to forget that file-disambiguation should be preferred, and that may take a while to catch, since it doesn't usually matter.
This is the move_to_san() routine from SjaakII,

Code: Select all

const char *move_to_san(move_t move, const movelist_t *movelist, char *buffer, bool san_castle)
{
   static char static_buffer[256];
   char *s = buffer;
   const char *gate_token = "";
   const char *token = "";
   const char *origin = "";
   char piece = ' ';
   char tp = '\0';

   if (!s) s = static_buffer;

   if (move == 0) {
      snprintf(s, sizeof static_buffer, "(pass)");
      return s;
   }

   int from = get_move_from(move);
   int to   = get_move_to(move);
   int p    = get_move_piece(move);
   piece = piece_symbol_string[p];

   if (san_castle && is_castle_move(move)) {
      int f = unpack_file(to);
      if (f >= div_file)
         snprintf(s, sizeof static_buffer, "%s", kingside_castle);
      else
         snprintf(s, sizeof static_buffer, "%s", queenside_castle);

      if (is_gate_move(move)) {
         snprintf(s + strlen(s), 256-strlen(s), "/%c%s", piece_symbol_string[get_move_promotion_piece(move)], square_names[get_move_drop_square(move)]);
      }
      return s;
   }
   if (abs(from-to) == 1 && is_castle_move(move)) {
      int from2 = get_castle_move_from2(move);
      int to2 = get_castle_move_to2(move);
      snprintf(s, sizeof static_buffer, "%cx%s-%s", piece, square_names[from2], square_names[to]);
      return s;
   }

   if (is_capture_move(move)) token = "x";
   if (is_drop_move(move)) token = "@";
   if (is_promotion_move(move) || is_gate_move(move)) tp = piece_symbol_string[get_move_promotion_piece(move)];
   if (is_gate_move(move)) gate_token = "/";

   if (is_double_capture_move(move)) {
      int from = get_move_from(move);
      int to = get_move_to(move);
      char fp = piece;
      int first = 1;
      int c1, c2;
      uint16_t p = 0;
   
      if (get_move_swaps(move))
         first = 0;

      p = get_move_pickup(move, first);
      c1 = decode_pickup_square(p);
      p = get_move_pickup(move, first+1);
      c2 = decode_pickup_square(p);
      if (c2 == to) {
         snprintf(s, 256, "%c%sx%sx%s", fp, square_names[from], square_names[c1], square_names[to]);
      } else {
         snprintf(s, 256, "%c%sx%sx%s-%s", fp, square_names[from], square_names[c1], square_names[c2], square_names[to]);
      }
      return s;
   }


   if (piece != ' ' && is_capture_move(move) && !is_double_capture_move(move) && get_move_capture_square(move)!=get_move_to(move)) {
      int from = get_move_from(move);
      int to = get_move_to(move);
      int cap = get_move_capture_square(move);
      char fp = piece_symbol_string[get_move_piece(move)];
      snprintf(s, 256, "%c%sx%s-%s", fp, "", square_names[cap], square_names[to]);
      return s;
   }


   if (is_drop_move(move)) piece = piece_drop_string[p];

   if (is_pickup_move(move)) {
      piece = piece_drop_string[p];
      token = "^"; 
      to = from;
      goto disambiguous;
   }

   if (is_drop_move(move)) goto disambiguous;

   /* Slightly special case: pawn capture */
   if (piece == ' ' && is_capture_move(move)) {
      origin = file_names[unpack_file(from)];
   } else if (movelist) {
      /* The information we have now might be ambiguous - check */
      int count = 0;
      int n;
      for (n=0; n<movelist->num_moves; n++) {
         if (is_drop_move(movelist->move[n])) continue;
         if (is_capture_move(movelist->move[n]) && !is_capture_move(move)) continue;
         if (!is_capture_move(movelist->move[n]) && is_capture_move(move)) continue;
         if (is_double_capture_move(movelist->move[n]) && !is_double_capture_move(move)) continue;
         if (!is_double_capture_move(movelist->move[n]) && is_double_capture_move(move)) continue;
         if (get_move_piece(move) == get_move_piece(movelist->move[n]) && to == get_move_to(movelist->move[n])) {
            if (is_promotion_move(move) && is_promotion_move(movelist->move[n]))
               count += tp == piece_symbol_string[get_move_promotion_piece(movelist->move[n])];
            else if (!is_promotion_move(move) && !is_promotion_move(movelist->move[n])) {
               if (is_gate_move(move) && is_gate_move(movelist->move[n]))
                  count += tp == piece_symbol_string[get_move_promotion_piece(movelist->move[n])];
               else if (!is_gate_move(move) && !is_gate_move(movelist->move[n]))
                  count++;
            }
         }
      }
      if (count <= 1) goto disambiguous;

      /* Try to disambiguate by file */
      count = 0;
      for (n=0; n<movelist->num_moves; n++) {
         if (is_drop_move(movelist->move[n])) continue;
         if (get_move_piece(move) == get_move_piece(movelist->move[n]) &&
             to == get_move_to(movelist->move[n]) &&
             unpack_file(from) == unpack_file(get_move_from(movelist->move[n])))
            count++;
      }
      if (count == 1) {
         origin = file_names[unpack_file(from)];
      } else if (count > 1) {
         /* Disambiguate by row */
         origin = rank_names[unpack_rank(from)];
      }
   }
disambiguous:

   snprintf(s, 15, "%c%s%s%s%s%c", piece, origin, token, square_names[to], gate_token, tp);
   if (piece == '+') {
      if (tp) tp = '+';
      snprintf(s, 15, "%c%c%s%s%s%s%c", piece, piece_psymbol_string[p], origin, token, square_names[to], gate_token, tp);
   }

   return s;
}
It has all manner of crap that you don't need to care about in a normal chess engine (drops, lifts, gates, double captures, captures on other squares than the destination, arbitrary promotions), but conceptually I think it's quite clean: it gathers information like piece ID string, whether the move is a capture, promotion pieces, near the top, then runs through a few special cases (castling, double captures, raids) before we're down to the basic "move piece from point A to point B".

EDIT:
This is the output (second column, the others are other move formats it understands) that it gives for the second position you specified:

Code: Select all

73 moves
  1/ 73 Na3-b1     Nb1        a3b1       a3b1      
  2/ 73 Na3-c2     Nac2       a3c2       a3c2      
  3/ 73 Na3-c4     Nac4       a3c4       a3c4      
  4/ 73 Na3-b5     Nb5        a3b5       a3b5      
  5/ 73 Ne3-d1     Nd1        e3d1       e3d1      
  6/ 73 Ne3-f1     Nf1        e3f1       e3f1      
  7/ 73 Ne3-c2     Nec2       e3c2       e3c2      
  8/ 73 Ne3-c4     Nec4       e3c4       e3c4      
  9/ 73 Ne3-g4     Ng4        e3g4       e3g4      
 10/ 73 Ne3-d5     Nd5        e3d5       e3d5      
 11/ 73  c7xb8Q     cxb8Q     c7b8q      c7b8q     
 12/ 73  c7xb8R     cxb8R     c7b8r      c7b8r     
 13/ 73  c7xb8B     cxb8B     c7b8b      c7b8b     
 14/ 73  c7xb8N     cxb8N     c7b8n      c7b8n     
 15/ 73  f5xe6      fxe6      f5e6       f5e6      
 16/ 73  d4xe5      dxe5      d4e5       d4e5      
 17/ 73  g2-g4      g4        g2g4       g2g4      
 18/ 73 Nd8-e6     Ne6        d8e6       d8e6      
 19/ 73 Nd8xb7     Nxb7       d8b7       d8b7      
 20/ 73 Ba6-f1     Bf1        a6f1       a6f1      
 21/ 73 Ba6-e2     Be2        a6e2       a6e2      
 22/ 73 Ba6-d3     Bd3        a6d3       a6d3      
 23/ 73 Ba6-c4     Bc4        a6c4       a6c4      
 24/ 73 Ba6-b5     Bab5       a6b5       a6b5      
 25/ 73 Ba6xb7     B6xb7      a6b7       a6b7      
 26/ 73 Bc6-a4     Ba4        c6a4       c6a4      
 27/ 73 Bc6-b5     Bcb5       c6b5       c6b5      
 28/ 73 Bc6-d5     Bd5        c6d5       c6d5      
 29/ 73 Bc6xb7     Bcxb7      c6b7       c6b7      
 30/ 73 Bc6xd7     Bxd7       c6d7       c6d7      
 31/ 73 Ba8xb7     B8xb7      a8b7       a8b7      
 32/ 73 Rh1-f1     Rf1        h1f1       h1f1      
 33/ 73 Rh1-g1     Rg1        h1g1       h1g1      
 34/ 73 Rh1-h2     R1h2       h1h2       h1h2      
 35/ 73 Rh1-h3     R1h3       h1h3       h1h3      
 36/ 73 Re4-f4     Ref4       e4f4       e4f4      
 37/ 73 Re4-g4     Reg4       e4g4       e4g4      
 38/ 73 Re4xe5     Rxe5       e4e5       e4e5      
 39/ 73 Rh4-h2     R4h2       h4h2       h4h2      
 40/ 73 Rh4-h3     R4h3       h4h3       h4h3      
 41/ 73 Rh4-f4     Rhf4       h4f4       h4f4      
 42/ 73 Rh4-g4     Rhg4       h4g4       h4g4      
 43/ 73 Rh4-h5     R4h5       h4h5       h4h5      
 44/ 73 Rh6-h5     R6h5       h6h5       h6h5      
 45/ 73 Rh6xh7     R6xh7      h6h7       h6h7      
 46/ 73 Rf7-e7     Re7        f7e7       f7e7      
 47/ 73 Rf7-g7     Rg7        f7g7       f7g7      
 48/ 73 Rf7-f8     Rff8       f7f8       f7f8      
 49/ 73 Rf7xd7     Rxd7       f7d7       f7d7      
 50/ 73 Rf7xh7     Rfxh7      f7h7       f7h7      
 51/ 73 Rh8-e8     Re8        h8e8       h8e8      
 52/ 73 Rh8-f8     Rhf8       h8f8       h8f8      
 53/ 73 Rh8-g8     Rg8        h8g8       h8g8      
 54/ 73 Rh8xh7     R8xh7      h8h7       h8h7      
 55/ 73 Qe1-a1     Qa1        e1a1       e1a1      
 56/ 73 Qe1-b1     Qb1        e1b1       e1b1      
 57/ 73 Qe1-c1     Qc1        e1c1       e1c1      
 58/ 73 Qe1-d1     Qd1        e1d1       e1d1      
 59/ 73 Qe1-f1     Qf1        e1f1       e1f1      
 60/ 73 Qe1-g1     Qg1        e1g1       e1g1      
 61/ 73 Qe1-e2     Qe2        e1e2       e1e2      
 62/ 73 Qe1-f2     Qf2        e1f2       e1f2      
 63/ 73 Qe1-g3     Qg3        e1g3       e1g3      
 64/ 73 Kg6-g5     Kg5        g6g5       g6g5      
 65/ 73 Kg6-h5     Kh5        g6h5       g6h5      
 66/ 73  g2-g3      g3        g2g3       g2g3      
 67/ 73 Kg6xh7     Kxh7       g6h7       g6h7      
 68/ 73  d4-d5      d5        d4d5       d4d5      
 69/ 73  c7-c8N     c8N       c7c8n      c7c8n     
 70/ 73  c7-c8B     c8B       c7c8b      c7c8b     
 71/ 73  c7-c8R     c8R       c7c8r      c7c8r     
 72/ 73  c7-c8Q     c8Q       c7c8q      c7c8q     
 73/ 73  d2-d3      d3        d2d3       d2d3      
mar
Posts: 2668
Joined: Fri Nov 26, 2010 2:00 pm
Location: Czech Republic
Full name: Martin Sedlak

Re: SAN test position

Post by mar »

Unfortunately, B6xb7 is ambiguous so it seems there is a bug in your SAN output as well (as was in mine :)
matthewlai
Posts: 793
Joined: Sun Aug 03, 2014 4:48 am
Location: London, UK

Re: SAN test position

Post by matthewlai »

Evert wrote:

Code: Select all

      /* Try to disambiguate by file */
      count = 0;
      for (n=0; n<movelist->num_moves; n++) {
         if (is_drop_move(movelist->move[n])) continue;
         if (get_move_piece(move) == get_move_piece(movelist->move[n]) &&
             to == get_move_to(movelist->move[n]) &&
             unpack_file(from) == unpack_file(get_move_from(movelist->move[n])))
            count++;
      }
      if (count == 1) {
         origin = file_names[unpack_file(from)];
      } else if (count > 1) {
         /* Disambiguate by row */
         origin = rank_names[unpack_rank(from)];
      }
   }
That doesn't work when you have a move that needs to be disambiguated by both file and rank.

[d]7k/8/8/1R3R2/8/3R4/8/K7 w - - 0 1

Rd3-d5 needs rank disambiguation
Rb5-d5 and Rf5-d5 both need both rank and file disambiguation.

This situation can only arise after 2 queen promotions or an under-promotion with both original pieces still alive.

And as Martin pointed out, my test position catches this :).
Last edited by matthewlai on Sun Sep 11, 2016 8:49 pm, edited 1 time in total.
Disclosure: I work for DeepMind on the AlphaZero project, but everything I say here is personal opinion and does not reflect the views of DeepMind / Alphabet.
mar
Posts: 2668
Joined: Fri Nov 26, 2010 2:00 pm
Location: Czech Republic
Full name: Martin Sedlak

Re: SAN test position

Post by mar »

matthewlai wrote:My disambiguation logic now looks like this:
Nice and elegant solution.
What I do (now that my SAN routine is fixed) for each other ambigous piece is:
- count different files
- count different rows
- count pieces

then:
if diff_files > 0 and diff_files == diff_count => diff_ranks = 0
if diff_ranks > 0 and diff_ranks == diff_count => diff_files = 0
mar
Posts: 2668
Joined: Fri Nov 26, 2010 2:00 pm
Location: Czech Republic
Full name: Martin Sedlak

Re: SAN test position

Post by mar »

matthewlai wrote: Rd3-d5 needs rank disambiguation
Rb5-d5 and Rf5-d5 both need both rank and file disambiguation.
What about Rbd5 and Rfd5 => you only need file disambiguation.
Rdd5 also only needs file disambiguation (rank would work as well but we prefer file)
my test position catches this :).
Indeed, a very nice test position! :)
matthewlai
Posts: 793
Joined: Sun Aug 03, 2014 4:48 am
Location: London, UK

Re: SAN test position

Post by matthewlai »

mar wrote:
matthewlai wrote: Rd3-d5 needs rank disambiguation
Rb5-d5 and Rf5-d5 both need both rank and file disambiguation.
What about Rbd5 and Rfd5 => you only need file disambiguation.
Rdd5 also only needs file disambiguation (rank would work as well but we prefer file)
my test position catches this :).
Indeed, a very nice test position! :)
Looks like my human SAN generator is buggy... my engine is fine, though! :).

I guess this can only happen with bishops, knights, and queens, because they don't move along files and ranks.

[d]7k/8/2B1B3/8/2B5/8/8/K7 w - - 0 1
Disclosure: I work for DeepMind on the AlphaZero project, but everything I say here is personal opinion and does not reflect the views of DeepMind / Alphabet.
mar
Posts: 2668
Joined: Fri Nov 26, 2010 2:00 pm
Location: Czech Republic
Full name: Martin Sedlak

Re: SAN test position

Post by mar »

matthewlai wrote:Looks like my human SAN generator is buggy... my engine is fine, though! :).
:)

Being more specific than necessary is not a bug IMHO.

Considering xboard protocol and communicating moves via SAN:
If the engine has a real bug (i.e. encoding ambiguous move), then the GUI
has two options:
- forfeit the game by illegal move
- choose any move at random (in this case it might not be the move the engine wanted to play)

That being said, SAN is probably not a good way to communicate moves between engines and GUIs.
It's more complicated and more bug-prone :)
jdart
Posts: 4413
Joined: Fri Mar 10, 2006 5:23 am
Location: http://www.arasanchess.org

Re: SAN test position

Post by jdart »

You might also consider using quantity instead of quality in your testing.

Write a PGN parser that uses your SAN parser as a component, and after parsing into your internal format, test for legality. Then output the PGN again using your SAN generator.

Feed it a large collection of known good games (TWIC is a good source although you might have to filter out Chess960 and other oddball games).

You should have no illegal moves and your output file should be parseable again by your program or by tools like pgn-extract.

I have put my own parser through millions of games this way.

--Jon