official-stockfish / Stockfish

A free and strong UCI chess engine
https://stockfishchess.org/
GNU General Public License v3.0
11.61k stars 2.28k forks source link

Move ordering issue (?) #2046

Closed Alayan-stk-2 closed 5 years ago

Alayan-stk-2 commented 5 years ago

I'm trying to write a patch to detect some rook+pawns fortress against queen (and queen+pawns). The pattern in current master is very limited and I'd like to expand it.

One of the pattern which is a guaranteed draw is : pawn protects pawn protects rook protects the pawn chain root ; with strong side king unable to go near the pawns due to the rook and weak side king next to the unbreakable structure. The queen can never force it away.

Example FEN : 8/8/3kp3/3p4/4r3/6K1/2Q5/8 w - - 0 1

So I did some (ugly) code in KQKRPs which checks for the above condition (certainly too restrictive but that's besides the point).

Using the example fen as a test, I observe a very weird behavior : 1)At depth 1, I get +0.13 eval (so the conditions and the SCALE_FACTOR_DRAW are properly triggered) 2)At depth >=2, I get back to an eval over +2 (as before the patch) 3)The PV see black do moves which, while keeping the draw, don't keep the conditions triggered, despite several available king moves resulting in positions which still trigger the patch conditions. I tested this by doing the move SF wanted for white, then choosing myself a move for black and inputting the resulting FEN in my SF : at depth 1 it gives a draw eval.

The only way I can explain this behavior is that my patched SF never considered any of the moves leading to positions still triggering the scale factor draw conditions.

Writing conditions detecting known draws seem kinda useless if move ordering and search will allow a 200cp fail low before considering the moves keeping the conditions active... I doubt too SF would do moves leading into triggering the conditions in the first place while searching from an earlier position with a few more pieces.

Changing the general move ordering heuristics to fix this specific situation would probably be disastrous, but maybe SF should use custom move ordering/pruning rules for specific kind of positions? That is, if position is of a specific type with custom heuristics, use the appropriate custom heuristics, otherwise fall back to default ones ?

Rocky640 commented 5 years ago

It might help it you provide a link to your code.

Alayan-stk-2 commented 5 years ago

Along with a few changes to the conditions in which the KQKRPs function is called (nothing major and I tested it to work as expected), the main change whose testing I'm talking about is below.

I'd be a bit surprised if there is one, but let me know if you can find a flaw which could explain the behavior I describe (says draw at depth 1, doesn't say it anymore at depth >=2 by doing a move which doesn't keep the condition triggered, but if forced to do a move keeping the condition active, eval it as draw at depth 1).

I did some runtime parameters changes and got this weird result though :

info depth 10 seldepth 16 multipv 1 score cp 177 nodes 16727 nps 929277 tbhits 0 time 18 pv c2b1 d6e7 b1b2 e7d6 b2b8 d6e7 b8c7 e7f6 c7c3 f6e7 c3g7 e7d6 g7f8 d6e5 f8b8 e5f6 b8d8 f6e5

In this PV, it looks like it should keep the conditions active but from the end position (5Q2/8/4p3/3pk3/4r3/6K1/8/8 w - - 18 10) it says 5 moves for white queen give +1.77 (the 3 non-losing checks, Qa3 and Qf3) while the rest is evaled as -0.13.

I didn't try to get tidy code at that WIP stage so it's rather inefficient and long, but is otherwise correct :

  constexpr Direction DownLeftW  = SOUTH_WEST;
  constexpr Direction DownRightW = SOUTH_EAST;
  constexpr Direction DownLeftB  = NORTH_EAST;
  constexpr Direction DownRightB = NORTH_WEST;

  Bitboard strongSidePawns = pos.pieces(strongSide, PAWN);
  Bitboard weakSidePawns = pos.pieces(weakSide, PAWN);

  Square kingSq = pos.square<KING>(weakSide);
  Square rsq = pos.square<ROOK>(weakSide);
  Rank rRank = relative_rank(weakSide, rsq);

  // Pawn protect pawn protect rook structures are unbreakable
  // for a lone queen. If there is one and the weak king
  // is close to it (thus can't be forced in a corner without moves)
  // the draw is guaranteed.
  if (   !strongSidePawns
      && relative_rank(weakSide, pos.square<KING>(strongSide)) > rRank
      && relative_rank(weakSide, kingSq) < rRank)
  {
      Bitboard is_PPR;
      if (weakSide == WHITE)
      {
          is_PPR =  (shift<DownLeftW>(shift<DownRightW>(pos.pieces(weakSide, ROOK)) & weakSidePawns) & weakSidePawns)
                  | (shift<DownRightW>(shift<DownLeftW>(pos.pieces(weakSide, ROOK)) & weakSidePawns) & weakSidePawns);
      }
      else
      {
          is_PPR =  (shift<DownLeftB>(shift<DownRightB>(pos.pieces(weakSide, ROOK)) & weakSidePawns) & weakSidePawns)
                  | (shift<DownRightB>(shift<DownLeftB>(pos.pieces(weakSide, ROOK)) & weakSidePawns) & weakSidePawns);
      }

      if(   is_PPR
         && (pos.attacks_from<KING>(kingSq) & weakSidePawns))
          return SCALE_FACTOR_DRAW;
  }
Rocky640 commented 5 years ago

Assume Black is the weak side, with pawns on e6 and d5 and Rook on c4

You are using constexpr Direction DownLeftB = NORTH_EAST; constexpr Direction DownRightB = NORTH_WEST;

So DownLeftB (c4) is d5, but then you compute DownRightB(d5) which is c6

So you are missing that important case !

The following code will work only if you test some positions with exactly 2 weakSidePawns, no need to change anything to bool is_KQKRPs

template<>
ScaleFactor Endgame<KQKRPs>::operator()(const Position& pos) const {

    Bitboard weakSidePawns = pos.pieces(weakSide, PAWN);

    Square kingSq = pos.square<KING>(weakSide);
    Square rsq = pos.square<ROOK>(weakSide);
    Rank rRank = relative_rank(weakSide, rsq);

    // Pawn protect pawn protect rook structures are unbreakable
    // for a lone queen. If there is one and the weak king
    // is close to it (thus can't be forced in a corner without moves)
    // the draw is guaranteed.
    if (   relative_rank(weakSide, pos.square<KING>(strongSide)) > rRank
        && relative_rank(weakSide, kingSq) < rRank)
    {
        Bitboard is_PP;
        if (weakSide == WHITE)
            is_PP = pawn_attacks_bb<WHITE>(pawn_attacks_bb<WHITE>(weakSidePawns) & weakSidePawns);
        else
            is_PP = pawn_attacks_bb<BLACK>(pawn_attacks_bb<BLACK>(weakSidePawns) & weakSidePawns);

        if ((is_PP & rsq)
            && (pos.attacks_from<KING>(kingSq) & weakSidePawns))
            return SCALE_FACTOR_DRAW;
    }

    return SCALE_FACTOR_NONE;
}
Alayan-stk-2 commented 5 years ago

I'm aware that the condition was exceedingly restrictive :

So I did some (ugly) code in KQKRPs which checks for the above condition (certainly too restrictive but that's besides the point).

It was an early experimentation.

Tonight I did a rewrite to include more situations and have a cleaner code. Due to being triggered in more situations, the draw scaling works much better (SF usually see the weak side can force it), but there are still positions where it fails to be triggered for reasons I have yet to uncover.

(On my test position given earlier in this thread, it always say draw from depth 12 onwards to depth 66, but it doesn't say so here : Q7/8/3pk3/4p3/3r4/3P4/8/4K3 w - - 0 1 ) EDIT : "info depth 1 seldepth 2 multipv 1 score cp 351 nodes 529 nps 264500 tbhits 0 time 2 pv a8e8 e6f6" The QS check evasion move is braindead stupid...

With sufficiently general conditions which don't require a very narrow path, the weird behavior I observed in my first post doesn't seem to happen anymore so I'll close this issue for now.