Variant-specific endgame evaluation (#823)
authorFabian Fichter <ianfab@users.noreply.github.com>
Thu, 22 Aug 2024 20:38:26 +0000 (22:38 +0200)
committerGitHub <noreply@github.com>
Thu, 22 Aug 2024 20:38:26 +0000 (22:38 +0200)
Add variant-specific endgame evaluation for antichess, atomic, duck chess, misere, and racing kings.

Closes #820

src/endgame.cpp
src/endgame.h
src/material.cpp
src/position.cpp
src/position.h
src/types.h
src/variant.cpp
src/variant.h
src/variants.ini

index a95ff37..06f18d3 100644 (file)
@@ -94,6 +94,31 @@ namespace Endgames {
     add<KSFK>("KSFK");
     add<KSFKF>("KSFKF");
     add<KRKS>("KRKS");
+    add<KCKR>("KCKR");
+    add<KAKR>("KAKR");
+
+    // Anti
+    add<RK, EG_EVAL_ANTI>("RvM");
+    add<KN, EG_EVAL_ANTI>("MvN");
+    add<NN, EG_EVAL_ANTI>("NvN");
+
+    // Atomic
+    add<KPK, EG_EVAL_ATOMIC>("MPvM");
+    add<KNK, EG_EVAL_ATOMIC>("MNvM");
+    add<KBK, EG_EVAL_ATOMIC>("MBvM");
+    add<KRK, EG_EVAL_ATOMIC>("MRvM");
+    add<KQK, EG_EVAL_ATOMIC>("MQvM");
+    add<KNNK, EG_EVAL_ATOMIC>("MNNvM");
+
+    // Duck
+    add<KBK, EG_EVAL_DUCK>("MBvM");
+    add<KNK, EG_EVAL_DUCK>("MNvM");
+    add<KPK, EG_EVAL_DUCK>("MPvM");
+
+    // Racing kings
+    add<KQK, EG_EVAL_RK>("KQK");
+    add<KRK, EG_EVAL_RK>("KRK");
+    add<KK, EG_EVAL_RK>("KK");
 
     add<KRPKB>("KRPKB");
     add<KBPKB>("KBPKB");
@@ -319,6 +344,42 @@ Value Endgame<KQKR>::operator()(const Position& pos) const {
 }
 
 
+/// KC vs KR. Drawish, but good winning chances if king and rook are close.
+template<>
+Value Endgame<KCKR>::operator()(const Position& pos) const {
+
+  assert(verify_material(pos, strongSide, ChancellorValueMg, 0));
+  assert(verify_material(pos, weakSide, RookValueMg, 0));
+
+  Square strongKing = pos.square<KING>(strongSide);
+  Square weakKing = pos.square<KING>(weakSide);
+  Square weakRook = pos.square<ROOK>(weakSide);
+
+  Value result =  Value(push_to_edge(weakKing, pos))
+                + push_close(strongKing, weakKing)
+                + push_close(weakRook, weakKing);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// KA vs KR. Very drawish.
+template<>
+Value Endgame<KAKR>::operator()(const Position& pos) const {
+
+  assert(verify_material(pos, strongSide, ArchbishopValueMg, 0));
+  assert(verify_material(pos, weakSide, RookValueMg, 0));
+
+  Square strongKing = pos.square<KING>(strongSide);
+  Square weakKing = pos.square<KING>(weakSide);
+
+  Value result =  Value(push_to_edge(weakKing, pos))
+                + push_close(strongKing, weakKing);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
 /// KNN vs KP. Very drawish, but there are some mate opportunities if we can
 /// press the weakSide King to a corner before the pawn advances too much.
 template<>
@@ -945,4 +1006,305 @@ ScaleFactor Endgame<KPKP>::operator()(const Position& pos) const {
   return Bitbases::probe(strongKing, strongPawn, weakKing, us) ? SCALE_FACTOR_NONE : SCALE_FACTOR_DRAW;
 }
 
+
+/// Endgame evals for special variants
+/// R vs K. The rook side always wins if there is no immediate forced capture.
+template<>
+Value Endgame<RK, EG_EVAL_ANTI>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ANTI);
+
+  Square RSq = pos.square<ROOK>(strongSide);
+  Square KSq = pos.square<COMMONER>(weakSide);
+
+  Value result = Value(push_to_edge(KSq, pos)) + push_close(RSq, KSq);
+
+  int dist_min = std::min(distance<Rank>(RSq, KSq), distance<File>(RSq, KSq));
+  int dist_max = std::max(distance<Rank>(RSq, KSq), distance<File>(RSq, KSq));
+
+  if (dist_min == 0)
+      result += strongSide == pos.side_to_move() || dist_max > 1 ? -VALUE_KNOWN_WIN : VALUE_KNOWN_WIN;
+  else if (dist_min == 1)
+      result += weakSide == pos.side_to_move() && dist_max > 1 ? -VALUE_KNOWN_WIN : VALUE_KNOWN_WIN;
+  else
+      result += VALUE_KNOWN_WIN;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// K vs N. The king usally wins, but there are a few exceptions.
+template<>
+Value Endgame<KN, EG_EVAL_ANTI>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ANTI);
+
+  Square KSq = pos.square<COMMONER>(strongSide);
+  Square NSq = pos.square<KNIGHT>(weakSide);
+
+  // wins for knight
+  if (pos.side_to_move() == strongSide && (attacks_bb<KNIGHT>(NSq) & KSq))
+      return -VALUE_KNOWN_WIN;
+  if (pos.side_to_move() == weakSide && (attacks_bb<KNIGHT>(NSq) & attacks_bb<KING>(KSq)))
+      return VALUE_KNOWN_WIN;
+
+  Value result = VALUE_KNOWN_WIN + push_to_edge(NSq, pos) - push_to_edge(KSq, pos);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+/// N vs N. The side to move always wins/loses if the knights are on
+/// same/opposite colored squares.
+template<>
+Value Endgame<NN, EG_EVAL_ANTI>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ANTI);
+
+  Square N1Sq = pos.square<KNIGHT>(pos.side_to_move());
+  Square N2Sq = pos.square<KNIGHT>(~pos.side_to_move());
+
+  Value result = VALUE_KNOWN_WIN + push_close(N1Sq, N2Sq);
+
+  return !opposite_colors(N1Sq, N2Sq) ? result : -result;
+}
+
+
+template<>
+Value Endgame<KXK, EG_EVAL_ATOMIC>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ATOMIC);
+
+  // Stalemate detection with lone king
+  if (pos.side_to_move() == weakSide && !MoveList<LEGAL>(pos).size())
+      return VALUE_DRAW;
+
+  Square winnerKSq = pos.square<COMMONER>(strongSide);
+  Square loserKSq = pos.square<COMMONER>(weakSide);
+
+  Value result =  pos.non_pawn_material(strongSide)
+                + pos.count<PAWN>(strongSide) * PawnValueEg
+                + push_to_edge(loserKSq, pos)
+                + push_away(winnerKSq, loserKSq);
+
+  // We need at least a major and a minor, or three minors to force checkmate
+  if (  ((pos.count<QUEEN>(strongSide) || pos.count<ROOK>(strongSide)) && pos.count<ALL_PIECES>(strongSide) >= 3)
+      || (pos.count<BISHOP>(strongSide) + pos.count<KNIGHT>(strongSide) >= 3
+         && (pos.count<KNIGHT>(strongSide) >= 2 || ((pos.pieces(strongSide, BISHOP) & DarkSquares)
+                                                    && (pos.pieces(strongSide, BISHOP) & ~DarkSquares)))))
+      result = std::min(result + VALUE_KNOWN_WIN, VALUE_MATE_IN_MAX_PLY - 1);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+template<>
+Value Endgame<KPK, EG_EVAL_ATOMIC>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ATOMIC);
+
+  Square winnerKSq = pos.square<COMMONER>(strongSide);
+  Square loserKSq = pos.square<COMMONER>(weakSide);
+
+  int dist = distance(winnerKSq, loserKSq);
+  // Draw in case of adjacent kings
+  if (dist <= (strongSide == pos.side_to_move() ? 1 : 2))
+      return VALUE_DRAW;
+
+  Value result = PawnValueEg
+                + 20 * relative_rank(strongSide, pos.square<PAWN>(strongSide), pos.max_rank()) - 20
+                + push_away(winnerKSq, loserKSq);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+template<> Value Endgame<KNK, EG_EVAL_ATOMIC>::operator()(const Position&) const { return VALUE_DRAW; }
+
+template<> Value Endgame<KBK, EG_EVAL_ATOMIC>::operator()(const Position&) const { return VALUE_DRAW; }
+
+template<> Value Endgame<KRK, EG_EVAL_ATOMIC>::operator()(const Position&) const { return VALUE_DRAW; }
+
+template<>
+Value Endgame<KQK, EG_EVAL_ATOMIC>::operator()(const Position& pos) const {
+
+  assert(pos.endgame_eval() == EG_EVAL_ATOMIC);
+
+  // Stalemate detection with lone king
+  if (pos.side_to_move() == weakSide && !MoveList<LEGAL>(pos).size())
+      return VALUE_DRAW;
+
+  Square winnerKSq = pos.square<COMMONER>(strongSide);
+  Square loserKSq = pos.square<COMMONER>(weakSide);
+
+  int dist = distance(winnerKSq, loserKSq);
+  // Draw in case of adjacent kings
+  // In the case of dist == 2, the square adjacent to both kings is ensured
+  // not be occupied by the queen, since eval is not called when in check.
+  if (dist <= (strongSide == pos.side_to_move() ? 1 : 2))
+      return VALUE_DRAW;
+
+  Value result =  pos.non_pawn_material(strongSide)
+                + push_to_edge(loserKSq, pos)
+                + push_away(winnerKSq, loserKSq);
+
+  if (dist >= (strongSide == pos.side_to_move() ? 3 : 4))
+      result += VALUE_KNOWN_WIN;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+template<> Value Endgame<KNNK, EG_EVAL_ATOMIC>::operator()(const Position&) const { return VALUE_DRAW; }
+
+
+/// Self-mate with KX vs KX.
+template<>
+Value Endgame<KXKX, EG_EVAL_MISERE>::operator()(const Position& pos) const {
+
+  assert(!pos.checkers()); // Eval is never called when in check
+
+  // Stalemate detection with lone king
+  if (pos.side_to_move() == weakSide && !MoveList<LEGAL>(pos).size())
+      return VALUE_DRAW;
+
+  Square strongKing = pos.square<KING>(strongSide);
+  Square weakKing   = pos.square<KING>(weakSide);
+
+  Value result =  pos.non_pawn_material(strongSide) * int(VALUE_KNOWN_WIN) / int(VALUE_KNOWN_WIN + pos.non_pawn_material(strongSide))
+                - pos.non_pawn_material(weakSide)
+                + pos.count<PAWN>(weakSide) * PawnValueEg
+                + push_to_opposing_edge(relative_square(weakSide, strongKing, pos.max_rank()), pos) * 2
+                + push_close(strongKing, weakKing) * 2;
+
+  for (Bitboard b = pos.pieces(PAWN); b;)
+  {
+      Square s = pop_lsb(b);
+      result += (push_close(strongKing, s) + push_close(weakKing, s)) / 2;
+  }
+
+  if (!pos.count<PAWN>(weakSide))
+      result = VALUE_DRAW;
+  else if (pos.count<PAWN>(weakSide) == 1)
+      result = result / 2;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// Mate with KX vs K. This function is used to evaluate positions with
+/// king and plenty of material vs a lone king. It simply gives the
+/// attacking side a bonus for driving the defending king towards the edge
+/// of the board, and for keeping the distance between the two kings small.
+template<>
+Value Endgame<KXK, EG_EVAL_DUCK>::operator()(const Position& pos) const {
+
+  Square strongKing = pos.square<COMMONER>(strongSide);
+  Square weakKing   = pos.square<COMMONER>(weakSide);
+
+  Value result =  pos.non_pawn_material(strongSide)
+                + pos.count<PAWN>(strongSide) * PawnValueEg
+                + push_to_edge(weakKing, pos)
+                + push_close(strongKing, weakKing);
+
+  result = std::min(result + VALUE_KNOWN_WIN, VALUE_TB_WIN_IN_MAX_PLY - 1);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// Drawish, but king should stay away from the edge
+template<>
+Value Endgame<KNK, EG_EVAL_DUCK>::operator()(const Position& pos) const {
+
+  Square strongKing = pos.square<COMMONER>(strongSide);
+  Square weakKing   = pos.square<COMMONER>(weakSide);
+
+  Value result =  Value(push_to_edge(weakKing, pos))
+                + push_close(strongKing, weakKing);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// Drawish, but king should stay away from the edge
+template<>
+Value Endgame<KBK, EG_EVAL_DUCK>::operator()(const Position& pos) const {
+
+  Square strongKing = pos.square<COMMONER>(strongSide);
+  Square weakKing   = pos.square<COMMONER>(weakSide);
+
+  Value result =  Value(push_to_edge(weakKing, pos))
+                + push_close(strongKing, weakKing);
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+
+/// Winning as long as pawn is safe
+template<>
+Value Endgame<KPK, EG_EVAL_DUCK>::operator()(const Position& pos) const {
+
+  Square strongKing = pos.square<COMMONER>(strongSide);
+  Square weakKing   = pos.square<COMMONER>(weakSide);
+  Square strongPawn = pos.square<PAWN>(strongSide);
+
+  Value result =  PawnValueEg + 50 * relative_rank(strongSide, strongPawn, pos.max_rank())
+                + push_to_edge(weakKing, pos)
+                + push_close(strongKing, weakKing)
+                + push_close(strongKing, strongPawn) / 2
+                + push_away(weakKing, strongPawn) / 2;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+/// Winning as long as last rank can be blocked
+template<>
+Value Endgame<KQK, EG_EVAL_RK>::operator()(const Position& pos) const {
+
+  Square strongKing  = pos.square<KING>(strongSide);
+  Square weakKing    = pos.square<KING>(weakSide);
+  Square strongQueen = pos.square<QUEEN>(strongSide);
+
+  Value result;
+
+  if (   rank_of(weakKing) < rank_of(strongQueen)
+      || rank_of(weakKing) + (weakSide == pos.side_to_move()) < RANK_7
+      || (Rank8BB & attacks_bb<QUEEN>(strongQueen, pos.pieces()) & ~(attacks_bb<QUEEN>(weakKing) | attacks_bb<SHOGI_KNIGHT>(weakKing))))
+      result = VALUE_KNOWN_WIN + 100 * rank_of(strongKing);
+  else
+      result = -VALUE_KNOWN_WIN;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+/// Winning as long as last rank can be blocked
+template<>
+Value Endgame<KRK, EG_EVAL_RK>::operator()(const Position& pos) const {
+
+  Square strongKing = pos.square<KING>(strongSide);
+  Square weakKing   = pos.square<KING>(weakSide);
+  Square strongRook = pos.square<ROOK>(strongSide);
+
+  Value result;
+
+  if (   rank_of(weakKing) < rank_of(strongRook)
+      || rank_of(weakKing) + (weakSide == pos.side_to_move()) < RANK_7
+      || (Rank8BB & attacks_bb<ROOK>(strongRook, pos.pieces()) & ~(attacks_bb<QUEEN>(weakKing) | attacks_bb<SHOGI_KNIGHT>(weakKing))))
+      result = VALUE_KNOWN_WIN + 100 * rank_of(strongKing);
+  else
+      result = -VALUE_KNOWN_WIN;
+
+  return strongSide == pos.side_to_move() ? result : -result;
+}
+
+/// KvK. Pure race
+template<>
+Value Endgame<KK, EG_EVAL_RK>::operator()(const Position& pos) const {
+
+  Square whiteKing = pos.square<KING>(WHITE);
+  Square blackKing = pos.square<KING>(BLACK);
+
+  Value result = (VALUE_KNOWN_WIN + 100 * std::max(rank_of(whiteKing), rank_of(blackKing)))
+                * std::clamp(rank_of(whiteKing) - rank_of(blackKing) - (pos.side_to_move() == BLACK), -1, 1);
+
+  return pos.side_to_move() == WHITE ? result : -result;
+}
+
 } // namespace Stockfish
index 52aaba6..aec3dcd 100644 (file)
@@ -54,6 +54,19 @@ enum EndgameCode {
   KSFK,  // KSF vs K
   KSFKF,  // KSF vs KF
   KRKS,  // KR vs KS
+  KCKR,  // KC vs KR
+  KAKR,  // KA vs KR
+
+  // Special
+  KXKX,
+  RK,
+  KN,
+  NN,
+  KQK,
+  KRK,
+  KBK,
+  KNK,
+  KK,
 
   SCALING_FUNCTIONS,
   KBPsK,   // KB and pawns vs K
@@ -72,7 +85,7 @@ enum EndgameCode {
 /// Endgame functions can be of two types depending on whether they return a
 /// Value or a ScaleFactor.
 
-template<EndgameCode E> using
+template<EndgameCode E, EndgameEval V = EG_EVAL_CHESS> using
 eg_type = typename std::conditional<(E < SCALING_FUNCTIONS), Value, ScaleFactor>::type;
 
 
@@ -89,7 +102,7 @@ struct EndgameBase {
 };
 
 
-template<EndgameCode E, typename T = eg_type<E>>
+template<EndgameCode E, EndgameEval V = EG_EVAL_CHESS, typename T = eg_type<E, V>>
 struct Endgame : public EndgameBase<T> {
 
   explicit Endgame(Color c) : EndgameBase<T>(c) {}
@@ -115,12 +128,12 @@ namespace Endgames {
     return std::get<std::is_same<T, ScaleFactor>::value>(maps);
   }
 
-  template<EndgameCode E, typename T = eg_type<E>>
+  template<EndgameCode E, EndgameEval V = EG_EVAL_CHESS, typename T = eg_type<E, V>>
   void add(const std::string& code) {
 
     StateInfo st;
-    map<T>()[Position().set(code, WHITE, &st).material_key()] = Ptr<T>(new Endgame<E>(WHITE));
-    map<T>()[Position().set(code, BLACK, &st).material_key()] = Ptr<T>(new Endgame<E>(BLACK));
+    map<T>()[Position().set(code, WHITE, &st).material_key(V)] = Ptr<T>(new Endgame<E, V>(WHITE));
+    map<T>()[Position().set(code, BLACK, &st).material_key(V)] = Ptr<T>(new Endgame<E, V>(BLACK));
   }
 
   template<typename T>
index 36f664e..bea9d46 100644 (file)
@@ -61,6 +61,9 @@ namespace {
   // the function maps because they correspond to more than one material hash key.
   Endgame<KFsPsK> EvaluateKFsPsK[] = { Endgame<KFsPsK>(WHITE), Endgame<KFsPsK>(BLACK) };
   Endgame<KXK>    EvaluateKXK[] = { Endgame<KXK>(WHITE),    Endgame<KXK>(BLACK) };
+  Endgame<KXK, EG_EVAL_ATOMIC> EvaluateKXKAtomic[] = { Endgame<KXK, EG_EVAL_ATOMIC>(WHITE), Endgame<KXK, EG_EVAL_ATOMIC>(BLACK) };
+  Endgame<KXK, EG_EVAL_DUCK> EvaluateKXKDuck[] = { Endgame<KXK, EG_EVAL_DUCK>(WHITE), Endgame<KXK, EG_EVAL_DUCK>(BLACK) };
+  Endgame<KXKX, EG_EVAL_MISERE> EvaluateKXKXMisere[] = { Endgame<KXKX, EG_EVAL_MISERE>(WHITE), Endgame<KXKX, EG_EVAL_MISERE>(BLACK) };
 
   Endgame<KBPsK>  ScaleKBPsK[]  = { Endgame<KBPsK>(WHITE),  Endgame<KBPsK>(BLACK) };
   Endgame<KQKRPs> ScaleKQKRPs[] = { Endgame<KQKRPs>(WHITE), Endgame<KQKRPs>(BLACK) };
@@ -80,6 +83,15 @@ namespace {
           && pos.non_pawn_material(us) >= std::min(RookValueMg, 2 * SilverValueMg);
   }
 
+  bool is_KXK_atomic(const Position& pos, Color us) {
+    return  !more_than_one(pos.pieces(~us))
+          && pos.non_pawn_material(us) >= RookValueMg + KnightValueMg;
+  }
+
+  bool is_KXKX(const Position& pos, Color us) {
+    return  pos.non_pawn_material(us) - pos.non_pawn_material(~us) > QueenValueMg;
+  }
+
   bool is_KBPsK(const Position& pos, Color us) {
     return   pos.non_pawn_material(us) == BishopValueMg
           && pos.count<PAWN>(us) >= 1;
@@ -146,7 +158,7 @@ namespace Material {
 
 Entry* probe(const Position& pos) {
 
-  Key key = pos.material_key();
+  Key key = pos.material_key(pos.endgame_eval());
   Entry* e = pos.this_thread()->materialTable[key];
 
   if (e->key == key)
@@ -176,83 +188,119 @@ Entry* probe(const Position& pos) {
   else
       e->gamePhase = Phase(((npm - EndgameLimit) * PHASE_MIDGAME) / (MidgameLimit - EndgameLimit));
 
-  if (pos.endgame_eval())
-  {
   // Let's look if we have a specialized evaluation function for this particular
   // material configuration. Firstly we look for a fixed configuration one, then
   // for a generic one if the previous search failed.
-  if ((e->evaluationFunction = Endgames::probe<Value>(key)) != nullptr)
+  if (pos.endgame_eval() && (e->evaluationFunction = Endgames::probe<Value>(key)) != nullptr)
       return e;
 
-  for (Color c : { WHITE, BLACK })
-      if (is_KFsPsK(pos, c))
+  switch (pos.endgame_eval())
+  {
+  case EG_EVAL_CHESS:
+      for (Color c : { WHITE, BLACK })
+          if (is_KFsPsK(pos, c))
+          {
+              e->evaluationFunction = &EvaluateKFsPsK[c];
+              return e;
+          }
+
+      for (Color c : { WHITE, BLACK })
+          if (is_KXK(pos, c))
+          {
+              e->evaluationFunction = &EvaluateKXK[c];
+              return e;
+          }
+
+      // OK, we didn't find any special evaluation function for the current material
+      // configuration. Is there a suitable specialized scaling function?
       {
-          e->evaluationFunction = &EvaluateKFsPsK[c];
-          return e;
-      }
+          const auto* sf = Endgames::probe<ScaleFactor>(key);
 
-  for (Color c : { WHITE, BLACK })
-      if (is_KXK(pos, c))
-      {
-          e->evaluationFunction = &EvaluateKXK[c];
-          return e;
+          if (sf)
+          {
+              e->scalingFunction[sf->strongSide] = sf; // Only strong color assigned
+              return e;
+          }
       }
 
-  // OK, we didn't find any special evaluation function for the current material
-  // configuration. Is there a suitable specialized scaling function?
-  const auto* sf = Endgames::probe<ScaleFactor>(key);
-
-  if (sf)
-  {
-      e->scalingFunction[sf->strongSide] = sf; // Only strong color assigned
-      return e;
-  }
-
-  // We didn't find any specialized scaling function, so fall back on generic
-  // ones that refer to more than one material distribution. Note that in this
-  // case we don't return after setting the function.
-  for (Color c : { WHITE, BLACK })
-  {
-    if (is_KBPsK(pos, c))
-        e->scalingFunction[c] = &ScaleKBPsK[c];
-
-    else if (is_KQKRPs(pos, c))
-        e->scalingFunction[c] = &ScaleKQKRPs[c];
-  }
-
-  if (npm_w + npm_b == VALUE_ZERO && pos.pieces(PAWN)) // Only pawns on the board
-  {
-      if (!pos.count<PAWN>(BLACK))
+      // We didn't find any specialized scaling function, so fall back on generic
+      // ones that refer to more than one material distribution. Note that in this
+      // case we don't return after setting the function.
+      for (Color c : { WHITE, BLACK })
       {
-          assert(pos.count<PAWN>(WHITE) >= 2);
+          if (is_KBPsK(pos, c))
+              e->scalingFunction[c] = &ScaleKBPsK[c];
 
-          e->scalingFunction[WHITE] = &ScaleKPsK[WHITE];
+          else if (is_KQKRPs(pos, c))
+              e->scalingFunction[c] = &ScaleKQKRPs[c];
       }
-      else if (!pos.count<PAWN>(WHITE))
-      {
-          assert(pos.count<PAWN>(BLACK) >= 2);
 
-          e->scalingFunction[BLACK] = &ScaleKPsK[BLACK];
-      }
-      else if (pos.count<PAWN>(WHITE) == 1 && pos.count<PAWN>(BLACK) == 1)
+      if (npm_w + npm_b == VALUE_ZERO && pos.pieces(PAWN)) // Only pawns on the board
       {
-          // This is a special case because we set scaling functions
-          // for both colors instead of only one.
-          e->scalingFunction[WHITE] = &ScaleKPKP[WHITE];
-          e->scalingFunction[BLACK] = &ScaleKPKP[BLACK];
+          if (!pos.count<PAWN>(BLACK))
+          {
+              assert(pos.count<PAWN>(WHITE) >= 2);
+
+              e->scalingFunction[WHITE] = &ScaleKPsK[WHITE];
+          }
+          else if (!pos.count<PAWN>(WHITE))
+          {
+              assert(pos.count<PAWN>(BLACK) >= 2);
+
+              e->scalingFunction[BLACK] = &ScaleKPsK[BLACK];
+          }
+          else if (pos.count<PAWN>(WHITE) == 1 && pos.count<PAWN>(BLACK) == 1)
+          {
+              // This is a special case because we set scaling functions
+              // for both colors instead of only one.
+              e->scalingFunction[WHITE] = &ScaleKPKP[WHITE];
+              e->scalingFunction[BLACK] = &ScaleKPKP[BLACK];
+          }
       }
-  }
-
-  // Zero or just one pawn makes it difficult to win, even with a small material
-  // advantage. This catches some trivial draws like KK, KBK and KNK and gives a
-  // drawish scale factor for cases such as KRKBP and KmmKm (except for KBBKN).
-  if (!pos.count<PAWN>(WHITE) && npm_w - npm_b <= BishopValueMg)
-      e->factor[WHITE] = uint8_t(npm_w <  RookValueMg && pos.count<ALL_PIECES>(WHITE) <= 2 ? SCALE_FACTOR_DRAW :
-                                 npm_b <= BishopValueMg && pos.count<ALL_PIECES>(WHITE) <= 3 ? 4 : 14);
 
-  if (!pos.count<PAWN>(BLACK) && npm_b - npm_w <= BishopValueMg)
-      e->factor[BLACK] = uint8_t(npm_b <  RookValueMg && pos.count<ALL_PIECES>(BLACK) <= 2 ? SCALE_FACTOR_DRAW :
-                                 npm_w <= BishopValueMg && pos.count<ALL_PIECES>(BLACK) <= 3 ? 4 : 14);
+      // Zero or just one pawn makes it difficult to win, even with a small material
+      // advantage. This catches some trivial draws like KK, KBK and KNK and gives a
+      // drawish scale factor for cases such as KRKBP and KmmKm (except for KBBKN).
+      if (!pos.count<PAWN>(WHITE) && npm_w - npm_b <= BishopValueMg)
+          e->factor[WHITE] = uint8_t(npm_w <  RookValueMg && pos.count<ALL_PIECES>(WHITE) <= 2 ? SCALE_FACTOR_DRAW :
+                                      npm_b <= BishopValueMg && pos.count<ALL_PIECES>(WHITE) <= 3 ? 4 : 14);
+
+      if (!pos.count<PAWN>(BLACK) && npm_b - npm_w <= BishopValueMg)
+          e->factor[BLACK] = uint8_t(npm_b <  RookValueMg && pos.count<ALL_PIECES>(BLACK) <= 2 ? SCALE_FACTOR_DRAW :
+                                      npm_w <= BishopValueMg && pos.count<ALL_PIECES>(BLACK) <= 3 ? 4 : 14);
+      break;
+  case EG_EVAL_ANTI:
+      break;
+  case EG_EVAL_ATOMIC:
+      for (Color c : { WHITE, BLACK })
+          if (is_KXK_atomic(pos, c))
+          {
+              e->evaluationFunction = &EvaluateKXKAtomic[c];
+              return e;
+          }
+      break;
+  case EG_EVAL_DUCK:
+      for (Color c : { WHITE, BLACK })
+          if (is_KXK(pos, c))
+          {
+              e->evaluationFunction = &EvaluateKXKDuck[c];
+              return e;
+          }
+      break;
+  case EG_EVAL_MISERE:
+      for (Color c : { WHITE, BLACK })
+          if (is_KXKX(pos, c))
+          {
+              e->evaluationFunction = &EvaluateKXKXMisere[c];
+              return e;
+          }
+      break;
+  case EG_EVAL_RK:
+      break;
+  case NO_EG_EVAL:
+      break;
+  default:
+      assert(false);
   }
 
   // Evaluate the material imbalance. We use PIECE_TYPE_NONE as a place holder
index df9aa61..5278c01 100644 (file)
@@ -45,6 +45,7 @@ namespace Zobrist {
   Key inHand[PIECE_NB][SQUARE_NB];
   Key checks[COLOR_NB][CHECKS_NB];
   Key wall[SQUARE_NB];
+  Key endgame[EG_EVAL_NB];
 }
 
 
@@ -178,6 +179,9 @@ void Position::init() {
   for (Square s = SQ_A1; s <= SQ_MAX; ++s)
       Zobrist::wall[s] = rng.rand<Key>();
 
+  for (int i = NO_EG_EVAL; i < EG_EVAL_NB; ++i)
+      Zobrist::endgame[i] = rng.rand<Key>();
+
   // Prepare the cuckoo tables
   std::memset(cuckoo, 0, sizeof(cuckoo));
   std::memset(cuckooMove, 0, sizeof(cuckooMove));
@@ -211,6 +215,10 @@ void Position::init() {
 #endif
 }
 
+Key Position::material_key(EndgameEval e) const {
+  return st->materialKey ^ Zobrist::endgame[e];
+}
+
 
 /// Position::set() initializes the position object with the given FEN string.
 /// This function is not very robust - make sure that input FENs are correct,
@@ -655,9 +663,7 @@ void Position::set_state(StateInfo* si) const {
 
 Position& Position::set(const string& code, Color c, StateInfo* si) {
 
-  assert(code[0] == 'K');
-
-  string sides[] = { code.substr(code.find('K', 1)),      // Weak
+  string sides[] = { code.substr(code.find('v') != string::npos ? code.find('v') + 1 : code.find('K', 1)),      // Weak
                      code.substr(0, std::min(code.find('v'), code.find('K', 1))) }; // Strong
 
   assert(sides[0].length() > 0 && sides[0].length() < 8);
@@ -665,9 +671,8 @@ Position& Position::set(const string& code, Color c, StateInfo* si) {
 
   std::transform(sides[c].begin(), sides[c].end(), sides[c].begin(), tolower);
 
-  string n = std::to_string(FILE_NB);
-  string fenStr =  n + "/" + sides[0] + char(FILE_NB - sides[0].length() + '0') + "/" + n + "/" + n + "/" + n + "/"
-                 + n + "/" + sides[1] + char(FILE_NB - sides[1].length() + '0') + "/" + n + " w - - 0 10";
+  string n = std::to_string(8);
+  string fenStr =  sides[0] + "///////" + sides[1] + " w - - 0 10";
 
   return set(variants.find("fairy")->second, fenStr, false, si, nullptr);
 }
@@ -1642,7 +1647,7 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
       k ^= Zobrist::psq[captured][capsq];
       st->materialKey ^= Zobrist::psq[captured][pieceCount[captured]];
 #ifndef NO_THREADS
-      prefetch(thisThread->materialTable[st->materialKey]);
+      prefetch(thisThread->materialTable[material_key(var->endgameEval)]);
 #endif
       // Reset rule 50 counter
       st->rule50 = 0;
index 712c21e..8868d76 100644 (file)
@@ -142,7 +142,7 @@ public:
   bool blast_on_capture() const;
   PieceSet blast_immune_types() const;
   PieceSet mutually_immune_types() const;
-  bool endgame_eval() const;
+  EndgameEval endgame_eval() const;
   Bitboard double_step_region(Color c) const;
   Bitboard triple_step_region(Color c) const;
   bool castling_enabled() const;
@@ -301,7 +301,7 @@ public:
   // Accessing hash keys
   Key key() const;
   Key key_after(Move m) const;
-  Key material_key() const;
+  Key material_key(EndgameEval e = EG_EVAL_CHESS) const;
   Key pawn_key() const;
 
   // Other properties of the position
@@ -507,9 +507,9 @@ inline PieceSet Position::mutually_immune_types() const {
   return var->mutuallyImmuneTypes;
 }
 
-inline bool Position::endgame_eval() const {
+inline EndgameEval Position::endgame_eval() const {
   assert(var != nullptr);
-  return var->endgameEval && !count_in_hand(ALL_PIECES) && count<KING>() == 2;
+  return !count_in_hand(ALL_PIECES) && (var->endgameEval != EG_EVAL_CHESS || count<KING>() == 2) ? var->endgameEval : NO_EG_EVAL;
 }
 
 inline Bitboard Position::double_step_region(Color c) const {
@@ -1326,10 +1326,6 @@ inline Key Position::pawn_key() const {
   return st->pawnKey;
 }
 
-inline Key Position::material_key() const {
-  return st->materialKey;
-}
-
 inline Score Position::psq_score() const {
   return psq;
 }
index b049736..b5812ab 100644 (file)
@@ -318,6 +318,10 @@ enum WallingRule {
   NO_WALLING, ARROW, DUCK, EDGE, PAST, STATIC
 };
 
+enum EndgameEval {
+  NO_EG_EVAL, EG_EVAL_CHESS, EG_EVAL_ANTI, EG_EVAL_ATOMIC, EG_EVAL_DUCK, EG_EVAL_MISERE, EG_EVAL_RK, EG_EVAL_NB
+};
+
 enum OptBool {
   NO_VALUE, VALUE_FALSE, VALUE_TRUE
 };
index db69f55..f6a8345 100644 (file)
@@ -138,6 +138,9 @@ namespace {
         Variant* v = chess_variant_base()->init();
         v->add_piece(SILVER, 's');
         v->add_piece(FERS, 'f');
+        v->add_piece(ARCHBISHOP, 'a');
+        v->add_piece(CHANCELLOR, 'c');
+        v->add_piece(COMMONER, 'm');
         return v;
     }
       // Raazuva (Maldivian Chess)
@@ -342,6 +345,7 @@ namespace {
         v->flagMove = true;
         v->castling = false;
         v->checking = false;
+        v->endgameEval = EG_EVAL_RK;
         return v;
     }
     // Knightmate
@@ -357,6 +361,15 @@ namespace {
         v->promotionPieceTypes[BLACK] = piece_set(COMMONER) | QUEEN | ROOK | BISHOP;
         return v;
     }
+    // Misere chess
+    // Get checkmated to win.
+    // Variant used to run some selfmate analysis http://www.kotesovec.cz/gustav/gustav_alybadix.htm
+    Variant* misere_variant() {
+        Variant* v = chess_variant_base()->init();
+        v->checkmateValue = VALUE_MATE;
+        v->endgameEval = EG_EVAL_MISERE;
+        return v;
+    }
     // Losers chess
     // https://www.chessclub.com/help/Wild17
     Variant* losers_variant() {
@@ -385,6 +398,7 @@ namespace {
         v->extinctionPieceTypes = piece_set(ALL_PIECES);
         v->mustCapture = true;
         v->nnueAlias = "antichess";
+        v->endgameEval = EG_EVAL_ANTI;
         return v;
     }
     // Antichess
@@ -393,6 +407,7 @@ namespace {
         Variant* v = giveaway_variant()->init();
         v->startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1";
         v->castling = false;
+        v->endgameEval = EG_EVAL_ANTI;
         return v;
     }
     // Suicide chess
@@ -402,6 +417,7 @@ namespace {
         Variant* v = antichess_variant()->init();
         v->stalematePieceCount = true;
         v->nnueAlias = "antichess";
+        v->endgameEval = EG_EVAL_ANTI;
         return v;
     }
     // Codrus
@@ -493,6 +509,7 @@ namespace {
     Variant* atomic_variant() {
         Variant* v = nocheckatomic_variant()->init();
         v->extinctionPseudoRoyal = true;
+        v->endgameEval = EG_EVAL_ATOMIC;
         return v;
     }
 
@@ -517,6 +534,7 @@ namespace {
         v->extinctionPieceTypes = piece_set(COMMONER);
         v->wallingRule = DUCK;
         v->stalemateValue = VALUE_MATE;
+        v->endgameEval = EG_EVAL_DUCK;
         return v;
     }
 #endif
@@ -1819,6 +1837,7 @@ void VariantMap::init() {
     add("kingofthehill", kingofthehill_variant());
     add("racingkings", racingkings_variant());
     add("knightmate", knightmate_variant());
+    add("misere", misere_variant());
     add("losers", losers_variant());
     add("giveaway", giveaway_variant());
     add("antichess", antichess_variant());
@@ -2027,21 +2046,26 @@ Variant* Variant::conclude() {
 
     // For endgame evaluation to be applicable, no special win rules must apply.
     // Furthermore, rules significantly changing game mechanics also invalidate it.
-    endgameEval = extinctionValue == VALUE_NONE
-                  && checkmateValue == -VALUE_MATE
-                  && stalemateValue == VALUE_DRAW
-                  && !materialCounting
-                  && !(flagRegion[WHITE] || flagRegion[BLACK])
-                  && !mustCapture
-                  && !checkCounting
-                  && !makpongRule
-                  && !connectN
-                  && !blastOnCapture
-                  && !petrifyOnCaptureTypes
-                  && !capturesToHand
-                  && !twoBoards
-                  && !restrictedMobility
-                  && kingType == KING;
+    endgameEval =  endgameEval != EG_EVAL_CHESS
+                 ||
+                   (   endgameEval == EG_EVAL_CHESS
+                    && extinctionValue == VALUE_NONE
+                    && checkmateValue == -VALUE_MATE
+                    && stalemateValue == VALUE_DRAW
+                    && !materialCounting
+                    && !(flagRegion[WHITE] || flagRegion[BLACK])
+                    && !mustCapture
+                    && !checkCounting
+                    && !makpongRule
+                    && !connectN
+                    && !blastOnCapture
+                    && !petrifyOnCaptureTypes
+                    && !capturesToHand
+                    && !twoBoards
+                    && !restrictedMobility
+                    && kingType == KING
+                   )
+                 ? endgameEval : NO_EG_EVAL;
 
     shogiStylePromotions = false;
     for (PieceType current: promotedPieceType)
index 03faa51..98fe314 100644 (file)
@@ -175,7 +175,7 @@ struct Variant {
   int pieceHandIndex[COLOR_NB][PIECE_NB];
   int kingSquareIndex[SQUARE_NB];
   int nnueMaxPieces;
-  bool endgameEval = false;
+  EndgameEval endgameEval = EG_EVAL_CHESS;
   bool shogiStylePromotions = false;
   std::vector<Direction> connect_directions;
   PieceSet connectPieceTypesTrimmed = ~NO_PIECE_SET;
@@ -222,6 +222,7 @@ struct Variant {
   // Reset values that always need to be redefined
   Variant* init() {
       nnueAlias = "";
+      endgameEval = EG_EVAL_CHESS;
       return this;
   }
 
index f00787e..d905dcb 100644 (file)
@@ -961,12 +961,6 @@ capturesToHand = true
 pocketSize = 6
 castling = false
 
-# Misere Chess
-# Get checkmated to win.
-# Variant used to run some selfmate analysis http://www.kotesovec.cz/gustav/gustav_alybadix.htm
-[misere:chess]
-checkmateValue = win
-
 # Chak
 # Variant invented by Couch Tomato and inspired in the Mayan civilization
 # https://www.pychess.org/variants/chak