Support pseudo-royal pieces
authorFabian Fichter <ianfab@users.noreply.github.com>
Sun, 28 Feb 2021 11:27:27 +0000 (12:27 +0100)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sun, 28 Feb 2021 12:37:04 +0000 (13:37 +0100)
This implements support for pseudo-royal pieces,
which allows to now fully support some new variants:
- lichess atomic rules
- coregal chess
- maharajah and the sepoys

Closes #81.

No functional change for other variants.

src/parser.cpp
src/position.cpp
src/position.h
src/variant.cpp
src/variant.h
src/variants.ini
tests/perft.sh

index 90590ad..bba3654 100644 (file)
@@ -298,6 +298,7 @@ Variant* VariantParser<DoCheck>::parse(Variant* v) {
     parse_attribute("bikjangRule", v->bikjangRule);
     parse_attribute("extinctionValue", v->extinctionValue);
     parse_attribute("extinctionClaim", v->extinctionClaim);
+    parse_attribute("extinctionPseudoRoyal", v->extinctionPseudoRoyal);
     // extinction piece types
     const auto& it_ext = config.find("extinctionPieceTypes");
     if (it_ext != config.end())
index 332aadf..ce812a4 100644 (file)
@@ -519,6 +519,17 @@ void Position::set_check_info(StateInfo* si) const {
   si->shak = si->checkersBB & (byTypeBB[KNIGHT] | byTypeBB[ROOK] | byTypeBB[BERS]);
   si->bikjang = var->bikjangRule && ksq != SQ_NONE ? bool(attacks_bb(sideToMove, ROOK, ksq, pieces()) & pieces(sideToMove, KING)) : false;
   si->legalCapture = NO_VALUE;
+  if (var->extinctionPseudoRoyal)
+  {
+      si->pseudoRoyals = 0;
+      for (PieceType pt : extinction_piece_types())
+      {
+          if (count(sideToMove, pt) <= var->extinctionPieceCount + 1)
+              si->pseudoRoyals |= pieces(sideToMove, pt);
+          if (count(~sideToMove, pt) <= var->extinctionPieceCount + 1)
+              si->pseudoRoyals |= pieces(~sideToMove, pt);
+      }
+  }
 }
 
 
@@ -944,6 +955,47 @@ bool Position::legal(Move m) const {
               return false;
   }
 
+  // Check for attacks to pseudo-royal pieces
+  if (var->extinctionPseudoRoyal)
+  {
+      Square kto = to;
+      if (type_of(m) == CASTLING && (st->pseudoRoyals & from))
+      {
+          // After castling, the rook and king final positions are the same in
+          // Chess960 as they would be in standard chess.
+          kto = make_square(to > from ? castling_kingside_file() : castling_queenside_file(), castling_rank(us));
+          Direction step = to > from ? WEST : EAST;
+          for (Square s = kto; s != from + step; s += step)
+              if (  !(blast_on_capture() && (attacks_bb<KING>(s) & st->pseudoRoyals & pieces(~sideToMove)))
+                  && attackers_to(s, (s == kto ? (pieces() ^ to) : pieces()) ^ from, ~us))
+                  return false;
+      }
+      Bitboard occupied = (type_of(m) != DROP ? pieces() ^ from : pieces()) | kto;
+      if (type_of(m) == EN_PASSANT)
+          occupied &= ~square_bb(kto - pawn_push(us));
+      if (capture(m) && blast_on_capture())
+          occupied &= ~((attacks_bb<KING>(kto) & (pieces() ^ pieces(PAWN))) | kto);
+      Bitboard pseudoRoyals = st->pseudoRoyals & pieces(sideToMove);
+      Bitboard pseudoRoyalsTheirs = st->pseudoRoyals & pieces(~sideToMove);
+      if (is_ok(from) && (pseudoRoyals & from))
+          pseudoRoyals ^= square_bb(from) ^ kto;
+      if (type_of(m) == PROMOTION && extinction_piece_types().find(promotion_type(m)) != extinction_piece_types().end())
+          pseudoRoyals |= kto;
+      // Self-explosions are illegal
+      if (pseudoRoyals & ~occupied)
+          return false;
+      // Check for legality unless we capture a pseudo-royal piece
+      if (!(pseudoRoyalsTheirs & ~occupied))
+          while (pseudoRoyals)
+          {
+              Square sr = pop_lsb(&pseudoRoyals);
+              // Touching pseudo-royal pieces are immune
+              if (  !(blast_on_capture() && (pseudoRoyalsTheirs & attacks_bb<KING>(sr)))
+                  && (attackers_to(sr, occupied, ~us) & (occupied & ~square_bb(kto))))
+                  return false;
+          }
+  }
+
   // En passant captures are a tricky special case. Because they are rather
   // uncommon, we do it simply by testing whether the king is attacked after
   // the move is made.
@@ -965,28 +1017,24 @@ bool Position::legal(Move m) const {
   // enemy attacks, it is delayed at a later time: now!
   if (type_of(m) == CASTLING)
   {
-      // Non-royal pieces can not be impeded from castling
-      if (type_of(piece_on(from)) != KING && !var->extinctionPseudoRoyal)
-          return true;
-
       // After castling, the rook and king final positions are the same in
       // Chess960 as they would be in standard chess.
       to = make_square(to > from ? castling_kingside_file() : castling_queenside_file(), castling_rank(us));
       Direction step = to > from ? WEST : EAST;
 
-      for (Square s = to; s != from; s += step)
-          if (attackers_to(s, ~us))
-              return false;
-
-      // TODO: need to consider touching kings
-      if (var->extinctionPseudoRoyal && attackers_to(from, ~us))
-          return false;
-
       // Will the gate be blocked by king or rook?
       Square rto = to + (to_sq(m) > from_sq(m) ? WEST : EAST);
       if (is_gating(m) && (gating_square(m) == to || gating_square(m) == rto))
           return false;
 
+      // Non-royal pieces can not be impeded from castling
+      if (type_of(piece_on(from)) != KING)
+          return true;
+
+      for (Square s = to; s != from; s += step)
+          if (attackers_to(s, ~us))
+              return false;
+
       // In case of Chess960, verify if the Rook blocks some checks
       // For instance an enemy queen in SQ_A1 when castling rook is in SQ_B1.
       return !chess960 || !attackers_to(to, pieces() ^ to_sq(m), ~us);
@@ -1013,7 +1061,7 @@ bool Position::legal(Move m) const {
   // square is attacked by the opponent. Castling moves are checked
   // for legality during move generation.
   if (type_of(moved_piece(m)) == KING)
-      return type_of(m) == CASTLING || !attackers_to(to, occupied, ~us);
+      return !attackers_to(to, occupied, ~us);
 
   Bitboard janggiCannons = pieces(JANGGI_CANNON);
   if (type_of(moved_piece(m)) == JANGGI_CANNON)
@@ -2273,8 +2321,9 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co
 
 bool Position::is_immediate_game_end(Value& result, int ply) const {
 
-  // extinction
-  if (extinction_value() != VALUE_NONE)
+  // Extinction
+  // Extinction does not apply for pseudo-royal pieces, because they can not be captured
+  if (extinction_value() != VALUE_NONE && (!var->extinctionPseudoRoyal || blast_on_capture()))
   {
       for (Color c : { ~sideToMove, sideToMove })
           for (PieceType pt : extinction_piece_types())
index e439ad7..8fa03e0 100644 (file)
@@ -68,6 +68,7 @@ struct StateInfo {
   Bitboard   pinners[COLOR_NB];
   Bitboard   checkSquares[PIECE_TYPE_NB];
   Bitboard   flippedPieces;
+  Bitboard   pseudoRoyals;
   OptBool    legalCapture;
   bool       capturedpromoted;
   bool       shak;
@@ -741,6 +742,19 @@ inline Value Position::stalemate_value(int ply) const {
       int c = count<ALL_PIECES>(sideToMove) - count<ALL_PIECES>(~sideToMove);
       return c == 0 ? VALUE_DRAW : convert_mate_value(c < 0 ? var->stalemateValue : -var->stalemateValue, ply);
   }
+  // Check for checkmate of pseudo-royal pieces
+  if (var->extinctionPseudoRoyal)
+  {
+      Bitboard pseudoRoyals = st->pseudoRoyals & pieces(sideToMove);
+      Bitboard pseudoRoyalsTheirs = st->pseudoRoyals & pieces(~sideToMove);
+      while (pseudoRoyals)
+      {
+          Square sr = pop_lsb(&pseudoRoyals);
+          if (  !(blast_on_capture() && (pseudoRoyalsTheirs & attacks_bb<KING>(sr)))
+              && attackers_to(sr, ~sideToMove))
+              return convert_mate_value(var->checkmateValue, ply);
+      }
+  }
   return convert_mate_value(var->stalemateValue, ply);
 }
 
index 7d500f0..80de6a0 100644 (file)
@@ -310,7 +310,6 @@ namespace {
     // https://en.wikipedia.org/wiki/Atomic_chess
     Variant* atomic_variant() {
         Variant* v = nocheckatomic_variant();
-        // TODO: castling, check(-mate), stalemate are not yet properly implemented
         v->extinctionPseudoRoyal = true;
         return v;
     }
@@ -615,6 +614,14 @@ namespace {
         v->shatarMateRule = true;
         return v;
     }
+    Variant* coregal_variant() {
+        Variant* v = fairy_variant();
+        v->extinctionValue = -VALUE_MATE;
+        v->extinctionPieceTypes = {QUEEN};
+        v->extinctionPseudoRoyal = true;
+        v->extinctionPieceCount = 64; // no matter how many queens, all are royal
+        return v;
+    }
     Variant* clobber_variant() {
         Variant* v = fairy_variant_base();
         v->pieceToCharTable = "P.................p.................";
@@ -1051,6 +1058,7 @@ void VariantMap::init() {
     add("almost", almost_variant()->conclude());
     add("chigorin", chigorin_variant()->conclude());
     add("shatar", shatar_variant()->conclude());
+    add("coregal", coregal_variant()->conclude());
     add("clobber", clobber_variant()->conclude());
     add("breakthrough", breakthrough_variant()->conclude());
     add("ataxx", ataxx_variant()->conclude());
index a68dac9..d8586dd 100644 (file)
@@ -115,7 +115,7 @@ struct Variant {
   bool bikjangRule = false;
   Value extinctionValue = VALUE_NONE;
   bool extinctionClaim = false;
-  bool extinctionPseudoRoyal = false; // TODO: implementation incomplete
+  bool extinctionPseudoRoyal = false;
   std::set<PieceType> extinctionPieceTypes = {};
   int extinctionPieceCount = 0;
   int extinctionOpponentPieceCount = 0;
index da9512f..d51171e 100644 (file)
 # bikjangRule: consider Janggi bikjang (facing kings) rule [bool] (default: false)
 # extinctionValue: result when one of extinctionPieceTypes is extinct [Value] (default: none)
 # extinctionClaim: extinction of opponent pieces can only be claimed as side to move [bool] (default: false)
+# extinctionPseudoRoyal: treat the last extinction piece like a royal piece [bool] (default: false)
 # extinctionPieceTypes: list of piece types for extinction rules, e.g., pnbrq (* means all) (default: )
 # extinctionPieceCount: piece count at which the game is decided by extinction rule (default: 0)
 # extinctionOpponentPieceCount: opponent piece count required to adjudicate by extinction rule (default: 0)
@@ -296,6 +297,22 @@ promotionPieceTypes = q
 doubleStep = false
 castling = false
 
+# Mahajarah and the Sepoys
+# https://en.wikipedia.org/wiki/Maharajah_and_the_Sepoys
+[maharajah]
+pawn = p
+knight = n
+bishop = b
+rook = r
+queen = q
+king = k
+amazon = m
+pieceToCharTable = PNBRQ.............MKpnbrq.............mk
+startFen = rnbqkbnr/pppppppp/8/8/8/8/8/4M3 w kq - 0 1
+extinctionValue = loss
+extinctionPieceTypes = m
+extinctionPseudoRoyal = true
+
 # Upside-down
 [upsidedown:chess]
 startFen = RNBKQBNR/PPPPPPPP/8/8/8/8/pppppppp/rnbkqbnr w - - 0 1
index b32f495..381191d 100755 (executable)
@@ -47,6 +47,13 @@ if [[ $1 == "" || $1 == "variant" ]]; then
   expect perft.exp racingkings startpos 4 296242 > /dev/null
   expect perft.exp racingkings "fen 6r1/2K5/5k2/8/3R4/8/8/8 w - - 0 1" 4 86041 > /dev/null
   expect perft.exp racingkings "fen 6R1/2k5/5K2/8/3r4/8/8/8 b - - 0 1" 4 86009 > /dev/null
+  expect perft.exp atomic startpos 4 197326 > /dev/null
+  expect perft.exp atomic "fen rn2kb1r/1pp1p2p/p2q1pp1/3P4/2P3b1/4PN2/PP3PPP/R2QKB1R b KQkq - 0 1" 4 1434825 > /dev/null
+  expect perft.exp atomic "fen rn1qkb1r/p5pp/2p5/3p4/N3P3/5P2/PPP4P/R1BQK3 w Qkq - 0 1" 4 714499 > /dev/null
+  expect perft.exp coregal startpos 4 195896 > /dev/null
+  expect perft.exp coregal "fen rn2kb1r/ppp1pppp/6q1/8/2PP2b1/5B2/PP3P1P/R1BQK1NR w KQkq - 1 9" 3 20421 > /dev/null
+  expect perft.exp coregal "fen 2Q5/3Pq2k/6p1/4Bp1p/5P1P/8/8/K7 w - - 2 72" 4 55970 > /dev/null
+  expect perft.exp coregal "fen r3kb1r/1pp1pppp/p1q2n2/3P4/6b1/2N2N2/PPP2PPP/R1BQ1RK1 b kq - 0 9" 4 136511 > /dev/null
   expect perft.exp knightmate startpos 5 3249033 > /dev/null
   expect perft.exp losers startpos 5 2723795 > /dev/null
   expect perft.exp antichess startpos 5 2732672 > /dev/null