Encode board steps in an unambiguous way
authorH.G.Muller <hgm@hgm-xboard.(none)>
Sat, 3 Jan 2026 18:10:11 +0000 (19:10 +0100)
committerH.G.Muller <hgm@hgm-xboard.(none)>
Sat, 3 Jan 2026 19:23:37 +0000 (20:23 +0100)
The steps/slider/hopper arrays that pass the leaps a piece can make from
the betza parser to the routines in bitboard.cpp that generate attack
sets from those are now encoded as if they are for a board of 32 files,
in a new integer type DirectionCode. Routines to convert this from and
to Direction are provided. This allows unambiguous encoding of moves by
the board step for boards of up to 16 files, while using the board step
itself already became ambiguous with leaps that moved 4 files.

src/bitboard.cpp
src/parser.cpp
src/piece.cpp
src/piece.h
src/psqt.cpp
src/types.h

index 4f00575..b590265 100644 (file)
@@ -96,40 +96,42 @@ namespace {
 #endif
 
   // Rider directions
-  const std::map<Direction, int> RookDirectionsV { {NORTH, 0}, {SOUTH, 0}};
-  const std::map<Direction, int> RookDirectionsH { {EAST, 0}, {WEST, 0} };
-  const std::map<Direction, int> BishopDirections { {NORTH_EAST, 0}, {SOUTH_EAST, 0}, {SOUTH_WEST, 0}, {NORTH_WEST, 0} };
-  const std::map<Direction, int> LameDabbabaDirections { {2 * NORTH, 0}, {2 * EAST, 0}, {2 * SOUTH, 0}, {2 * WEST, 0} };
-  const std::map<Direction, int> HorseDirections { {2 * SOUTH + WEST, 0}, {2 * SOUTH + EAST, 0}, {SOUTH + 2 * WEST, 0}, {SOUTH + 2 * EAST, 0},
-                                                   {NORTH + 2 * WEST, 0}, {NORTH + 2 * EAST, 0}, {2 * NORTH + WEST, 0}, {2 * NORTH + EAST, 0} };
-  const std::map<Direction, int> ElephantDirections { {2 * NORTH_EAST, 0}, {2 * SOUTH_EAST, 0}, {2 * SOUTH_WEST, 0}, {2 * NORTH_WEST, 0} };
-  const std::map<Direction, int> JanggiElephantDirections { {NORTH + 2 * NORTH_EAST, 0}, {EAST  + 2 * NORTH_EAST, 0},
-                                                            {EAST  + 2 * SOUTH_EAST, 0}, {SOUTH + 2 * SOUTH_EAST, 0},
-                                                            {SOUTH + 2 * SOUTH_WEST, 0}, {WEST  + 2 * SOUTH_WEST, 0},
-                                                            {WEST  + 2 * NORTH_WEST, 0}, {NORTH + 2 * NORTH_WEST, 0} };
-  const std::map<Direction, int> GrasshopperDirectionsV { {NORTH, 1}, {SOUTH, 1}};
-  const std::map<Direction, int> GrasshopperDirectionsH { {EAST, 1}, {WEST, 1} };
-  const std::map<Direction, int> GrasshopperDirectionsD { {NORTH_EAST, 1}, {SOUTH_EAST, 1}, {SOUTH_WEST, 1}, {NORTH_WEST, 1} };
+  const std::map<DirectionCode, int> RookDirectionsV { {step(1, 0), 0}, {step(-1, 0), 0}};
+  const std::map<DirectionCode, int> RookDirectionsH { {step(0, 1), 0}, {step(0, -1), 0} };
+  const std::map<DirectionCode, int> BishopDirections { {step(1, 1), 0}, {step(-1, 1), 0}, {step(-1, -1), 0}, {step(1, -1), 0} };
+  const std::map<DirectionCode, int> LameDabbabaDirections { {step(2, 0), 0}, {step(0, 2), 0}, {step(-2, 0), 0}, {step(0, -2), 0} };
+  const std::map<DirectionCode, int> HorseDirections { {step(-2, 1), 0}, {step(-2, -1), 0}, {step(-1, 2), 0}, {step(-1, -2), 0},
+                                                   {step(1, -2), 0}, {step(1, 2), 0}, {step(2, -1), 0}, {step(2, 1), 0} };
+  const std::map<DirectionCode, int> ElephantDirections { {step(2, 2), 0}, {step(-2, 2), 0}, {step(-2, -2), 0}, {step(2, -2), 0} };
+  const std::map<DirectionCode, int> JanggiElephantDirections { {step(3, 2), 0}, {step(2, 3), 0},
+                                                            {step(-2, 3), 0}, {step(-3, 2), 0},
+                                                            {step(-3, -2), 0}, {step(-2, -3), 0},
+                                                            {step(2, -3), 0}, {step(3, -2), 0} };
+  const std::map<DirectionCode, int> GrasshopperDirectionsV { {step(1, 0), 0xFFFE}, {step(-1, 0), 0xFFFE}};
+  const std::map<DirectionCode, int> GrasshopperDirectionsH { {step(0, 1), 0xFFFE}, {step(0, -1), 0xFFFE} };
+  const std::map<DirectionCode, int> GrasshopperDirectionsD { {step(1, 1), 0xFFFE}, {step(-1, 1), 0xFFFE},
+                                                              {step(-1, -1), 0xFFFE}, {step(1, -1), 0xFFFE} };
 
   enum MovementType { RIDER, HOPPER, LAME_LEAPER, HOPPER_RANGE };
 
   template <MovementType MT>
 #ifdef PRECOMPUTED_MAGICS
-  void init_magics(Bitboard table[], Magic magics[], std::map<Direction, int> directions, const Bitboard magicsInit[]);
+  void init_magics(Bitboard table[], Magic magics[], std::map<DirectionCode, int> directions, const Bitboard magicsInit[]);
 #else
-  void init_magics(Bitboard table[], Magic magics[], std::map<Direction, int> directions);
+  void init_magics(Bitboard table[], Magic magics[], std::map<DirectionCode, int> directions);
 #endif
 
   template <MovementType MT>
-  Bitboard sliding_attack(std::map<Direction, int> directions, Square sq, Bitboard occupied, Color c = WHITE) {
+  Bitboard sliding_attack(std::map<DirectionCode, int> directions, Square sq, Bitboard occupied, Color c = WHITE) {
     assert(MT != LAME_LEAPER);
 
     Bitboard attack = 0;
 
-    for (auto const& [d, limit] : directions)
+    for (auto const& [v, limit] : directions)
     {
         int count = 0;
         bool hurdle = false;
+        Direction d = board_step(v);
         for (Square s = sq + (c == WHITE ? d : -d);
              is_ok(s) && distance(s, s - (c == WHITE ? d : -d)) <= 2;
              s += (c == WHITE ? d : -d))
@@ -156,10 +158,10 @@ namespace {
     return attack;
   }
 
-  Bitboard lame_leaper_path(Direction d, Square s) {
+  Bitboard lame_leaper_path(DirectionCode d, Square s) {
     Direction dr = d > 0 ? NORTH : SOUTH;
-    Direction df = (std::abs(d % NORTH) < NORTH / 2 ? d % NORTH : -(d % NORTH)) < 0 ? WEST : EAST;
-    Square to = s + d;
+    Direction df = h_step(d) < 0 ? WEST : EAST;
+    Square to = s + board_step(d);
     Bitboard b = 0;
     if (!is_ok(to) || distance(s, to) >= 4)
         return b;
@@ -179,19 +181,21 @@ namespace {
     return b;
   }
 
-  Bitboard lame_leaper_path(std::map<Direction, int> directions, Square s) {
+  Bitboard lame_leaper_path(std::map<DirectionCode, int> directions, Square s) {
     Bitboard b = 0;
     for (const auto& i : directions)
         b |= lame_leaper_path(i.first, s);
     return b;
   }
 
-  Bitboard lame_leaper_attack(std::map<Direction, int> directions, Square s, Bitboard occupied) {
+  Bitboard lame_leaper_attack(std::map<DirectionCode, int> directions, Square s, Bitboard occupied) {
     Bitboard b = 0;
     for (const auto& i : directions)
     {
-        Square to = s + i.first;
-        if (is_ok(to) && distance(s, to) < 4 && !(lame_leaper_path(i.first, s) & occupied))
+        Direction d = board_step(i.first);
+        Direction h = h_step(i.first);
+        Square to = s + d;
+        if (is_ok(to) && file_of(s) + h < FILE_NB && file_of(s) + h >= 0 && !(lame_leaper_path(i.first, s) & occupied))
             b |= to;
     }
     return b;
@@ -202,9 +206,11 @@ namespace {
 /// safe_destination() returns the bitboard of target square for the given step
 /// from the given square. If the step is off the board, returns empty bitboard.
 
-inline Bitboard safe_destination(Square s, int step) {
+inline Bitboard safe_destination(Square s, DirectionCode c) {
+    Direction step = board_step(c);
+    Direction h = h_step(c);
     Square to = Square(s + step);
-    return is_ok(to) && distance(s, to) <= 3 ? square_bb(to) : Bitboard(0);
+    return is_ok(to) && file_of(s) + h >= 0 && file_of(s) + h < FILE_NB ? square_bb(to) : Bitboard(0);
 }
 
 
@@ -297,9 +303,9 @@ void Bitboards::init_pieces() {
                       leaper = 0;
                       for (auto const& [d, limit] : pi->steps[initial][modality])
                       {
-                          pseudo |= safe_destination(s, c == WHITE ? d : -d);
+                          pseudo |= safe_destination(s, DirectionCode(c == WHITE ? d : -d));
                           if (!limit)
-                              leaper |= safe_destination(s, c == WHITE ? d : -d);
+                              leaper |= safe_destination(s, DirectionCode(c == WHITE ? d : -d));
                       }
                       pseudo |= sliding_attack<RIDER>(pi->slider[initial][modality], s, 0, c);
                       pseudo |= sliding_attack<HOPPER_RANGE>(pi->hopper[initial][modality], s, 0, c);
@@ -388,9 +394,9 @@ namespace {
 
   template <MovementType MT>
 #ifdef PRECOMPUTED_MAGICS
-  void init_magics(Bitboard table[], Magic magics[], std::map<Direction, int> directions, const Bitboard magicsInit[]) {
+  void init_magics(Bitboard table[], Magic magics[], std::map<DirectionCode, int> directions, const Bitboard magicsInit[]) {
 #else
-  void init_magics(Bitboard table[], Magic magics[], std::map<Direction, int> directions) {
+  void init_magics(Bitboard table[], Magic magics[], std::map<DirectionCode, int> directions) {
 #endif
 
     // Optimal PRNG seeds to pick the correct magics in the shortest time
index cbd2404..b7829b7 100644 (file)
@@ -627,7 +627,7 @@ Variant* VariantParser<DoCheck>::parse(Variant* v) {
                     || pi->hopper[0][MODALITY_CAPTURE].size()
                     || std::any_of(pi->steps[0][MODALITY_CAPTURE].begin(),
                                    pi->steps[0][MODALITY_CAPTURE].end(),
-                                   [](const std::pair<const Direction, int>& d) { return d.second; }))
+                                   [](const std::pair<const DirectionCode, int>& d) { return d.second; }))
                     std::cerr << piece_name(v->kingType) << " is not supported as kingType." << std::endl;
             }
         }
index 1a48f80..c78075c 100644 (file)
@@ -148,22 +148,22 @@ namespace {
                       auto has_dir = [&](std::string s) {
                         return std::find(directions.begin(), directions.end(), s) != directions.end();
                       };
-                      if (directions.size() == 0 || has_dir("ff") || has_dir("vv") || has_dir("rf") || has_dir("rv") || has_dir("fh") || has_dir("rh") || has_dir("hr"))
-                          v[Direction(atom.first * FILE_NB + atom.second)] = distance;
-                      if (directions.size() == 0 || has_dir("bb") || has_dir("vv") || has_dir("lb") || has_dir("lv") || has_dir("bh") || has_dir("lh") || has_dir("hr"))
-                          v[Direction(-atom.first * FILE_NB - atom.second)] = distance;
-                      if (directions.size() == 0 || has_dir("rr") || has_dir("ss") || has_dir("br") || has_dir("bs") || has_dir("bh") || has_dir("rh") || has_dir("hr"))
-                          v[Direction(-atom.second * FILE_NB + atom.first)] = distance;
-                      if (directions.size() == 0 || has_dir("ll") || has_dir("ss") || has_dir("fl") || has_dir("fs") || has_dir("fh") || has_dir("lh") || has_dir("hr"))
-                          v[Direction(atom.second * FILE_NB - atom.first)] = distance;
-                      if (directions.size() == 0 || has_dir("rr") || has_dir("ss") || has_dir("fr") || has_dir("fs") || has_dir("fh") || has_dir("rh") || has_dir("hl"))
-                          v[Direction(atom.second * FILE_NB + atom.first)] = distance;
-                      if (directions.size() == 0 || has_dir("ll") || has_dir("ss") || has_dir("bl") || has_dir("bs") || has_dir("bh") || has_dir("lh") || has_dir("hl"))
-                          v[Direction(-atom.second * FILE_NB - atom.first)] = distance;
-                      if (directions.size() == 0 || has_dir("bb") || has_dir("vv") || has_dir("rb") || has_dir("rv") || has_dir("bh") || has_dir("rh") || has_dir("hl"))
-                          v[Direction(-atom.first * FILE_NB + atom.second)] = distance;
-                      if (directions.size() == 0 || has_dir("ff") || has_dir("vv") || has_dir("lf") || has_dir("lv") || has_dir("fh") || has_dir("lh") || has_dir("hl"))
-                          v[Direction(atom.first * FILE_NB - atom.second)] = distance;
+                       if (directions.size() == 0 || has_dir("ff") || has_dir("vv") || has_dir("rf") || has_dir("rv") || has_dir("fh") || has_dir("rh") || has_dir("hr"))
+                          v[step(y, x)] = distance;
+                       if (directions.size() == 0 || has_dir("bb") || has_dir("vv") || has_dir("lb") || has_dir("lv") || has_dir("bh") || has_dir("lh") || has_dir("hr"))
+                          v[step(-y, -x)] = distance;
+                       if (directions.size() == 0 || has_dir("rr") || has_dir("ss") || has_dir("br") || has_dir("bs") || has_dir("bh") || has_dir("rh") || has_dir("hr"))
+                          v[step(-x, y)] = distance;
+                       if (directions.size() == 0 || has_dir("ll") || has_dir("ss") || has_dir("fl") || has_dir("fs") || has_dir("fh") || has_dir("lh") || has_dir("hr"))
+                          v[step(x, -y)] = distance;
+                       if (directions.size() == 0 || has_dir("rr") || has_dir("ss") || has_dir("fr") || has_dir("fs") || has_dir("fh") || has_dir("rh") || has_dir("hl"))
+                          v[step(x, y)] = distance;
+                       if (directions.size() == 0 || has_dir("ll") || has_dir("ss") || has_dir("bl") || has_dir("bs") || has_dir("bh") || has_dir("lh") || has_dir("hl"))
+                          v[step(-x, -y)] = distance;
+                       if (directions.size() == 0 || has_dir("bb") || has_dir("vv") || has_dir("rb") || has_dir("rv") || has_dir("bh") || has_dir("rh") || has_dir("hl"))
+                          v[step(-y, x)] = distance;
+                       if (directions.size() == 0 || has_dir("ff") || has_dir("vv") || has_dir("lf") || has_dir("lv") || has_dir("fh") || has_dir("lh") || has_dir("hl"))
+                          v[step(y, -x)] = distance;
                   }
               }
               // Reset state
index 8d14dfd..38fdcc9 100644 (file)
@@ -34,9 +34,9 @@ enum MoveModality {MODALITY_QUIET, MODALITY_CAPTURE, MOVE_MODALITY_NB};
 struct PieceInfo {
   std::string name = "";
   std::string betza = "";
-  std::map<Direction, int> steps[2][MOVE_MODALITY_NB] = {};
-  std::map<Direction, int> slider[2][MOVE_MODALITY_NB] = {};
-  std::map<Direction, int> hopper[2][MOVE_MODALITY_NB] = {};
+  std::map<DirectionCode, int> steps[2][MOVE_MODALITY_NB] = {};
+  std::map<DirectionCode, int> slider[2][MOVE_MODALITY_NB] = {};
+  std::map<DirectionCode, int> hopper[2][MOVE_MODALITY_NB] = {};
 };
 
 struct PieceMap : public std::map<PieceType, const PieceInfo*> {
@@ -52,6 +52,22 @@ inline std::string piece_name(PieceType pt) {
                        : pieceMap.find(pt)->second->name;
 }
 
+inline DirectionCode step(int y, int x) {
+  return DirectionCode(y * WIDTH + x);
+}
+
+inline Direction h_step(DirectionCode v) {
+  return Direction(((v + HWIDTH) & X_STEP_MASK) - HWIDTH);
+}
+
+inline Direction v_step(DirectionCode v) {
+  return Direction((v - h_step(v)) / WIDTH);
+}
+
+inline Direction board_step(DirectionCode v) {
+  return Direction(v_step(v) * FILE_NB + h_step(v));
+}
+
 } // namespace Stockfish
 
 #endif // #ifndef PIECE_H_INCLUDED
index d1a84d6..0732c4d 100644 (file)
@@ -154,7 +154,7 @@ constexpr Score PBonus[RANK_NB][FILE_NB] =
 
 
 // Scale down slider value based on distance
-int slider_fraction(std::map<Direction, int> slider) {
+int slider_fraction(std::map<DirectionCode, int> slider) {
     int s = 0;
     for (auto const& [_, limit] : slider) {
         s += limit == 0 ? 100 : 200 * std::min(limit + 1, 8) / 16;
@@ -175,8 +175,8 @@ Value piece_value(Phase phase, PieceType pt)
             + (phase == MG ? 100 :  80) * pi->hopper[0][MODALITY_CAPTURE].size()
             + (phase == MG ?  85 :  60) * pi->hopper[0][MODALITY_QUIET].size()
             // Rook sliding directions are more valuable, especially in endgame
-            + (phase == MG ?  15 :  15) * std::count_if(pi->slider[0][MODALITY_CAPTURE].begin(), pi->slider[0][MODALITY_CAPTURE].end(), [](const std::pair<const Direction, int>& d) { return std::abs(d.first) == NORTH || std::abs(d.first) == 1; })
-            + (phase == MG ?  30 :  50) * std::count_if(pi->slider[0][MODALITY_QUIET].begin(), pi->slider[0][MODALITY_QUIET].end(), [](const std::pair<const Direction, int>& d) { return std::abs(d.first) == NORTH || std::abs(d.first) == 1; });
+            + (phase == MG ?  15 :  15) * std::count_if(pi->slider[0][MODALITY_CAPTURE].begin(), pi->slider[0][MODALITY_CAPTURE].end(), [](const std::pair<const DirectionCode, int>& d) { return std::abs(board_step(d.first)) == NORTH || std::abs(board_step(d.first)) == 1; })
+            + (phase == MG ?  30 :  50) * std::count_if(pi->slider[0][MODALITY_QUIET].begin(), pi->slider[0][MODALITY_QUIET].end(), [](const std::pair<const DirectionCode, int>& d) { return std::abs(board_step(d.first)) == NORTH || std::abs(board_step(d.first)) == 1; });
     return Value(v0 * exp(double(v0) / 10000));
 }
 
@@ -227,8 +227,8 @@ void init(const Variant* v) {
       
       const PieceInfo* pi = pieceMap.find(pt)->second;
       bool isSlider = pi->slider[0][MODALITY_QUIET].size() || pi->slider[0][MODALITY_CAPTURE].size() || pi->hopper[0][MODALITY_QUIET].size() || pi->hopper[0][MODALITY_CAPTURE].size();
-      bool isPawn = !isSlider && pi->steps[0][MODALITY_QUIET].size() && !std::any_of(pi->steps[0][MODALITY_QUIET].begin(), pi->steps[0][MODALITY_QUIET].end(), [](const std::pair<const Direction, int>& d) { return d.first < SOUTH / 2; });
-      bool isSlowLeaper = !isSlider && !std::any_of(pi->steps[0][MODALITY_QUIET].begin(), pi->steps[0][MODALITY_QUIET].end(), [](const std::pair<const Direction, int>& d) { return dist(d.first) > 1; });
+      bool isPawn = !isSlider && pi->steps[0][MODALITY_QUIET].size() && !std::any_of(pi->steps[0][MODALITY_QUIET].begin(), pi->steps[0][MODALITY_QUIET].end(), [](const std::pair<const DirectionCode, int>& d) { return board_step(d.first) < SOUTH / 2; });
+      bool isSlowLeaper = !isSlider && !std::any_of(pi->steps[0][MODALITY_QUIET].begin(), pi->steps[0][MODALITY_QUIET].end(), [](const std::pair<const DirectionCode, int>& d) { return dist(board_step(d.first)) > 1; });
 
       // Scale slider piece values with board size
       if (isSlider)
@@ -261,7 +261,7 @@ void init(const Variant* v) {
       // Increase leapers' value in makpong
       else if (v->makpongRule)
       {
-          if (std::any_of(pi->steps[0][MODALITY_CAPTURE].begin(), pi->steps[0][MODALITY_CAPTURE].end(), [](const std::pair<const Direction, int>& d) { return dist(d.first) > 1 && !d.second; }))
+          if (std::any_of(pi->steps[0][MODALITY_CAPTURE].begin(), pi->steps[0][MODALITY_CAPTURE].end(), [](const std::pair<const DirectionCode, int>& d) { return dist(board_step(d.first)) > 1 && !d.second; }))
               score = make_score(mg_value(score) * 4200 / (3500 + mg_value(score)),
                                  eg_value(score) * 4700 / (3500 + mg_value(score)));
       }
index b5812ab..e271faf 100644 (file)
@@ -543,6 +543,12 @@ enum Direction : int {
   NORTH_WEST = NORTH + WEST
 };
 
+enum DirectionCode : int {
+  HWIDTH = 16,
+  WIDTH = 2*HWIDTH,
+  X_STEP_MASK = WIDTH - 1
+};
+
 enum File : int {
 #ifdef LARGEBOARDS
   FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H, FILE_I, FILE_J, FILE_K, FILE_L,