Support atomic chess without checks (#81)
authorFabian Fichter <ianfab@users.noreply.github.com>
Tue, 19 Jan 2021 19:09:15 +0000 (20:09 +0100)
committerFabian Fichter <ianfab@users.noreply.github.com>
Tue, 19 Jan 2021 20:32:32 +0000 (21:32 +0100)
Add variant nocheckatomic which implements a modified version
of atomic chess where check and check-/stalemate do not apply,
which is very similar to the rules used on ICC.

The variant atomic is an incomplete implementation of
lichess atomic chess rules, added for easier usage in GUIs.
It will occasionally play illegal moves in this variant.

README.md
src/evaluate.cpp
src/parser.cpp
src/position.cpp
src/position.h
src/psqt.cpp
src/ucioption.cpp
src/variant.cpp
src/variant.h
src/variants.ini

index 7e5c8b9..a5e84e4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ The games currently supported besides chess are listed below. Fairy-Stockfish ca
 - [King of the Hill](https://en.wikipedia.org/wiki/King_of_the_Hill_(chess)), [Racing Kings](https://en.wikipedia.org/wiki/V._R._Parton#Racing_Kings)
 - [Three-check](https://en.wikipedia.org/wiki/Three-check_chess), Five-check
 - [Los Alamos](https://en.wikipedia.org/wiki/Los_Alamos_chess)
+- [Atomic](https://en.wikipedia.org/wiki/Atomic_chess) (non-standard rules)
 - [Horde](https://en.wikipedia.org/wiki/Dunsany%27s_Chess#Horde_Chess)
 - [Knightmate](https://www.chessvariants.com/diffobjective.dir/knightmate.html)
 
index 239cf2e..ce1495a 100644 (file)
@@ -857,12 +857,31 @@ namespace {
     if (pos.extinction_value() == -VALUE_MATE)
     {
         Bitboard bExt = attackedBy[Us][ALL_PIECES] & pos.pieces(Them);
-        while (bExt)
+        for (PieceType pt : pos.extinction_piece_types())
         {
-            PieceType pt = type_of(pos.piece_on(pop_lsb(&bExt)));
+            if (pt == ALL_PIECES)
+                continue;
             int denom = std::max(pos.count_with_hand(Them, pt) - pos.extinction_piece_count(), 1);
-            if (pos.extinction_piece_types().find(pt) != pos.extinction_piece_types().end())
-                score += make_score(1000, 1000) / (denom * denom);
+            // Explosion threats
+            if (pos.blast_on_capture())
+            {
+                int evasions = popcount(((attackedBy[Them][pt] & ~pos.pieces(Them)) | pos.pieces(Them, pt)) & ~attackedBy[Us][ALL_PIECES]) * denom;
+                int attacks = popcount((attackedBy[Them][pt] | pos.pieces(Them, pt)) & attackedBy[Us][ALL_PIECES]);
+                int explosions = 0;
+
+                Bitboard bExtBlast = bExt & (attackedBy2[Us] | ~attackedBy[Us][pt]);
+                while (bExtBlast)
+                {
+                    Square s = pop_lsb(&bExtBlast);
+                    if (((attacks_bb<KING>(s) | s) & pos.pieces(Them, pt)) && !(attacks_bb<KING>(s) & pos.pieces(Us, pt)))
+                        explosions++;
+                }
+                int danger = 20 * attacks / (evasions + 1) + 40 * explosions;
+                score += make_score(danger * (100 + danger), 0);
+            }
+            else
+                // Direct extinction threats
+                score += make_score(1000, 1000) / (denom * denom) * popcount(bExt & pos.pieces(Them, pt));
         }
     }
 
index 9da29fd..d45c763 100644 (file)
@@ -237,6 +237,7 @@ Variant* VariantParser<DoCheck>::parse(Variant* v) {
     parse_attribute("mandatoryPawnPromotion", v->mandatoryPawnPromotion);
     parse_attribute("mandatoryPiecePromotion", v->mandatoryPiecePromotion);
     parse_attribute("pieceDemotion", v->pieceDemotion);
+    parse_attribute("blastOnCapture", v->blastOnCapture);
     parse_attribute("endgameEval", v->endgameEval);
     parse_attribute("doubleStep", v->doubleStep);
     parse_attribute("doubleStepRank", v->doubleStepRank);
@@ -365,6 +366,18 @@ Variant* VariantParser<DoCheck>::parse(Variant* v) {
                     std::cerr << "pieceToCharTable - Missing piece type: " << ptu << std::endl;
             }
         }
+
+        // Check for limitations
+
+        // Options incompatible with royal kings
+        if (v->pieceTypes.find(KING) != v->pieceTypes.end())
+        {
+            if (v->blastOnCapture)
+                std::cerr << "Can not use kings with blastOnCapture" << std::endl;
+            if (v->flipEnclosedPieces)
+                std::cerr << "Can not use kings with flipEnclosedPieces" << std::endl;
+        }
+
     }
     return v;
 }
index 38b2cc6..72df161 100644 (file)
@@ -948,7 +948,7 @@ bool Position::legal(Move m) const {
   if (type_of(m) == CASTLING)
   {
       // Non-royal pieces can not be impeded from castling
-      if (type_of(piece_on(from)) != KING)
+      if (type_of(piece_on(from)) != KING && !var->extinctionPseudoRoyal)
           return true;
 
       // After castling, the rook and king final positions are the same in
@@ -960,6 +960,10 @@ bool Position::legal(Move m) const {
           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))
@@ -1590,6 +1594,59 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
           st->gatesBB[us] = 0;
   }
 
+  // Remove the blast pieces
+  if (captured && blast_on_capture())
+  {
+      std::memset(st->unpromotedBycatch, 0, sizeof(st->unpromotedBycatch));
+      st->demotedBycatch = st->promotedBycatch = 0;
+      Bitboard blast = (attacks_bb<KING>(to) & (pieces() ^ pieces(PAWN))) | to;
+      while (blast)
+      {
+          Square bsq = pop_lsb(&blast);
+          Piece bpc = piece_on(bsq);
+          Color bc = color_of(bpc);
+          if (type_of(bpc) != PAWN)
+              st->nonPawnMaterial[bc] -= PieceValue[MG][bpc];
+
+          // Update board and piece lists
+          // In order to not have to store the values of both board and unpromotedBoard,
+          // demote promoted pieces, but keep promoted pawns as promoted,
+          // and store demotion/promotion bitboards to disambiguate the piece state
+          bool capturedPromoted = is_promoted(bsq);
+          Piece unpromotedCaptured = unpromoted_piece_on(bsq);
+          st->unpromotedBycatch[bsq] = unpromotedCaptured ? unpromotedCaptured : bpc;
+          if (unpromotedCaptured)
+              st->demotedBycatch |= bsq;
+          else if (capturedPromoted)
+              st->promotedBycatch |= bsq;
+          remove_piece(bsq);
+          board[bsq] = NO_PIECE;
+          if (captures_to_hand())
+          {
+              Piece pieceToHand = !capturedPromoted || drop_loop() ? ~bpc
+                                 : unpromotedCaptured ? ~unpromotedCaptured
+                                                      : make_piece(~color_of(bpc), PAWN);
+              add_to_hand(pieceToHand);
+              k ^=  Zobrist::inHand[pieceToHand][pieceCountInHand[color_of(pieceToHand)][type_of(pieceToHand)] - 1]
+                  ^ Zobrist::inHand[pieceToHand][pieceCountInHand[color_of(pieceToHand)][type_of(pieceToHand)]];
+          }
+
+          // Update material hash key
+          k ^= Zobrist::psq[bpc][bsq];
+          st->materialKey ^= Zobrist::psq[bpc][pieceCount[bpc]];
+          if (type_of(bpc) == PAWN)
+              st->pawnKey ^= Zobrist::psq[bpc][bsq];
+
+          // Update castling rights if needed
+          if (st->castlingRights && castlingRightsMask[bsq])
+          {
+              int cr = castlingRightsMask[bsq];
+              k ^= Zobrist::castling[st->castlingRights & cr];
+              st->castlingRights &= ~cr;
+          }
+      }
+  }
+
   // Update the key with the final value
   st->key = k;
   // Calculate checkers bitboard (if move gives check)
@@ -1650,6 +1707,29 @@ void Position::undo_move(Move m) {
          || (is_pass(m) && pass()));
   assert(type_of(st->capturedPiece) != KING);
 
+  // Add the blast pieces
+  if (st->capturedPiece && blast_on_capture())
+  {
+      Bitboard blast = attacks_bb<KING>(to) | to;
+      while (blast)
+      {
+          Square bsq = pop_lsb(&blast);
+          Piece unpromotedBpc = st->unpromotedBycatch[bsq];
+          Piece bpc = st->demotedBycatch & bsq ? make_piece(color_of(unpromotedBpc), promoted_piece_type(type_of(unpromotedBpc)))
+                                               : unpromotedBpc;
+          bool isPromoted = (st->promotedBycatch | st->demotedBycatch) & bsq;
+
+          // Update board and piece lists
+          if (bpc)
+          {
+              put_piece(bpc, bsq, isPromoted, st->demotedBycatch & bsq ? unpromotedBpc : NO_PIECE);
+              if (captures_to_hand())
+                  remove_from_hand(!drop_loop() && (st->promotedBycatch & bsq) ? make_piece(~color_of(unpromotedBpc), PAWN)
+                                                                               : ~unpromotedBpc);
+          }
+      }
+  }
+
   // Remove gated piece
   if (is_gating(m))
   {
@@ -1856,6 +1936,53 @@ Key Position::key_after(Move m) const {
 }
 
 
+Value Position::blast_see(Move m) const {
+  assert(is_ok(m));
+
+  Square from = from_sq(m);
+  Square to = to_sq(m);
+  Color us = color_of(moved_piece(m));
+  Bitboard fromto = type_of(m) == DROP ? square_bb(to) : from | to;
+  Bitboard blast = ((attacks_bb<KING>(to) & ~pieces(PAWN)) | fromto) & pieces();
+
+  Value result = VALUE_ZERO;
+
+  // Add the least valuable attacker for quiet moves
+  if (!capture(m))
+  {
+      Bitboard attackers = attackers_to(to, pieces() ^ fromto, ~us);
+      Value minAttacker = VALUE_INFINITE;
+
+      while (attackers)
+      {
+          Square s = pop_lsb(&attackers);
+          if (extinction_piece_types().find(type_of(piece_on(s))) == extinction_piece_types().end())
+              minAttacker = std::min(minAttacker, blast & s ? VALUE_ZERO : PieceValue[MG][piece_on(s)]);
+      }
+
+      if (minAttacker == VALUE_INFINITE)
+          return VALUE_ZERO;
+
+      result += minAttacker;
+      if (type_of(m) == DROP)
+          result -= PieceValue[MG][dropped_piece_type(m)];
+  }
+
+  // Sum up blast piece values
+  while (blast)
+  {
+      Piece bpc = piece_on(pop_lsb(&blast));
+      if (extinction_piece_types().find(type_of(bpc)) != extinction_piece_types().end())
+          return color_of(bpc) == us ?  extinction_value()
+                        : capture(m) ? -extinction_value()
+                                     : VALUE_ZERO;
+      result += color_of(bpc) == us ? -PieceValue[MG][bpc] : PieceValue[MG][bpc];
+  }
+
+  return capture(m) || must_capture() ? result - 1 : std::min(result, VALUE_ZERO);
+}
+
+
 /// Position::see_ge (Static Exchange Evaluation Greater or Equal) tests if the
 /// SEE value of move is greater or equal to the given threshold. We'll use an
 /// algorithm similar to alpha-beta pruning with a null window.
@@ -1869,10 +1996,15 @@ bool Position::see_ge(Move m, Value threshold) const {
       return VALUE_ZERO >= threshold;
 
   Square from = from_sq(m), to = to_sq(m);
+
   // nCheck
   if (check_counting() && color_of(moved_piece(m)) == sideToMove && gives_check(m))
       return true;
 
+  // Atomic explosion SEE
+  if (blast_on_capture())
+      return blast_see(m) >= threshold;
+
   // Extinction
   if (   extinction_value() != VALUE_NONE
       && piece_on(to)
@@ -1882,6 +2014,7 @@ bool Position::see_ge(Move m, Value threshold) const {
               && count<ALL_PIECES>(~sideToMove) == extinction_piece_count() + 1)))
       return extinction_value() < VALUE_ZERO;
 
+  // Do not evaluate SEE if value would be unreliable
   if (must_capture() || !checking_permitted() || is_gating(m) || count<CLOBBER_PIECE>() == count<ALL_PIECES>())
       return VALUE_ZERO >= threshold;
 
@@ -2117,7 +2250,7 @@ bool Position::is_immediate_game_end(Value& result, int ply) const {
   // extinction
   if (extinction_value() != VALUE_NONE)
   {
-      for (Color c : { WHITE, BLACK })
+      for (Color c : { ~sideToMove, sideToMove })
           for (PieceType pt : extinction_piece_types())
               if (   count_with_hand( c, pt) <= var->extinctionPieceCount
                   && count_with_hand(~c, pt) >= var->extinctionOpponentPieceCount + (extinction_claim() && c == sideToMove))
index 688b6de..3f65599 100644 (file)
@@ -58,6 +58,9 @@ struct StateInfo {
   Bitboard   checkersBB;
   Piece      capturedPiece;
   Piece      unpromotedCapturedPiece;
+  Piece      unpromotedBycatch[SQUARE_NB];
+  Bitboard   promotedBycatch;
+  Bitboard   demotedBycatch;
   StateInfo* previous;
   Bitboard   blockersForKing[COLOR_NB];
   Bitboard   pinners[COLOR_NB];
@@ -122,6 +125,7 @@ public:
   bool mandatory_pawn_promotion() const;
   bool mandatory_piece_promotion() const;
   bool piece_demotion() const;
+  bool blast_on_capture() const;
   bool endgame_eval() const;
   bool double_step_enabled() const;
   Rank double_step_rank_max() const;
@@ -251,6 +255,7 @@ public:
   void undo_null_move();
 
   // Static Exchange Evaluation
+  Value blast_see(Move m) const;
   bool see_ge(Move m, Value threshold = VALUE_ZERO) const;
 
   // Accessing hash keys
@@ -422,6 +427,11 @@ inline bool Position::piece_demotion() const {
   return var->pieceDemotion;
 }
 
+inline bool Position::blast_on_capture() const {
+  assert(var != nullptr);
+  return var->blastOnCapture;
+}
+
 inline bool Position::endgame_eval() const {
   assert(var != nullptr);
   return var->endgameEval && !count_in_hand(WHITE, ALL_PIECES) && !count_in_hand(BLACK, ALL_PIECES);
index aa05b32..929f175 100644 (file)
@@ -129,7 +129,11 @@ void init(const Variant* v) {
 
       // Consider promotion types in pawn score
       if (pt == PAWN)
+      {
           score -= make_score(0, (QueenValueEg - maxPromotion) / 100);
+          if (v->blastOnCapture)
+              score += score;
+      }
 
       // Scale slider piece values with board size
       const PieceInfo* pi = pieceMap.find(pt)->second;
@@ -168,6 +172,9 @@ void init(const Variant* v) {
       else if (v->twoBoards)
           score = make_score(mg_value(score) * 7000 / (7000 + mg_value(score)),
                              eg_value(score) * 7000 / (7000 + eg_value(score)));
+      else if (v->blastOnCapture)
+          score = make_score(mg_value(score) * 7000 / (7000 + mg_value(score)),
+                             eg_value(score) * 7000 / (7000 + eg_value(score)));
       else if (v->checkCounting)
           score = make_score(mg_value(score) * (40000 + mg_value(score)) / 41000,
                              eg_value(score) * (30000 + eg_value(score)) / 31000);
index cf923b0..9314daa 100644 (file)
@@ -45,7 +45,7 @@ namespace UCI {
 // standard variants of XBoard/WinBoard
 std::set<string> standard_variants = {
     "normal", "nocastle", "fischerandom", "knightmate", "3check", "makruk", "shatranj",
-    "asean", "seirawan", "crazyhouse", "bughouse", "suicide", "giveaway", "losers",
+    "asean", "seirawan", "crazyhouse", "bughouse", "suicide", "giveaway", "losers", "atomic",
     "capablanca", "gothic", "janus", "caparandom", "grand", "shogi", "xiangqi"
 };
 
index d0eb095..aef593d 100644 (file)
@@ -289,6 +289,26 @@ namespace {
         v->extinctionPieceTypes = {ALL_PIECES};
         return v;
     }
+    // Atomic chess without checks (ICC rules)
+    // https://www.chessclub.com/help/atomic
+    Variant* nocheckatomic_variant() {
+        Variant* v = fairy_variant_base();
+        v->variantTemplate = "atomic";
+        v->remove_piece(KING);
+        v->add_piece(COMMONER, 'k');
+        v->extinctionValue = -VALUE_MATE;
+        v->extinctionPieceTypes = {COMMONER};
+        v->blastOnCapture = true;
+        return v;
+    }
+    // Atomic chess
+    // 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;
+    }
     Variant* threecheck_variant() {
         Variant* v = fairy_variant_base();
         v->startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 3+3 0 1";
@@ -998,6 +1018,8 @@ void VariantMap::init() {
     add("kinglet", kinglet_variant()->conclude());
     add("threekings", threekings_variant()->conclude());
     add("horde", horde_variant()->conclude());
+    add("nocheckatomic", nocheckatomic_variant()->conclude());
+    add("atomic", atomic_variant()->conclude());
     add("3check", threecheck_variant()->conclude());
     add("5check", fivecheck_variant()->conclude());
     add("crazyhouse", crazyhouse_variant()->conclude());
index ce4ee80..f6da799 100644 (file)
@@ -54,6 +54,7 @@ struct Variant {
   bool mandatoryPawnPromotion = true;
   bool mandatoryPiecePromotion = false;
   bool pieceDemotion = false;
+  bool blastOnCapture = false;
   bool endgameEval = false;
   bool doubleStep = true;
   Rank doubleStepRank = RANK_2;
@@ -112,6 +113,7 @@ struct Variant {
   bool bikjangRule = false;
   Value extinctionValue = VALUE_NONE;
   bool extinctionClaim = false;
+  bool extinctionPseudoRoyal = false; // TODO: implementation incomplete
   std::set<PieceType> extinctionPieceTypes = {};
   int extinctionPieceCount = 0;
   int extinctionOpponentPieceCount = 0;
index b77c932..7375492 100644 (file)
 # mandatoryPawnPromotion: pawn promotion is mandatory [bool] (default: true)
 # mandatoryPiecePromotion: piece promotion (and demotion if enabled) is mandatory [bool] (default: false)
 # pieceDemotion: enable demotion of pieces (e.g., Kyoto shogi) [bool] (default: false)
+# blastOnCapture: captures explode all adjacent non-pawn pieces (e.g., atomic chess) [bool] (default: false)
 # endgameEval: enable special endgame evaluation (for very chess-like variants only) [bool] (default: false)
 # doubleStep: enable pawn double step [bool] (default: true)
 # doubleStepRank: relative rank from where pawn double steps are allowed [Rank] (default: 2)