Answers.Body: <p>Created a script that will detect the following tactical themes.</p>
  <ul>
  <li>pins</li>
  <li>forks</li>
  <li>double check</li>
  <li>undermine</li>
  </ul>
  <p>It will read the games in the pgn file. Visit each position in the game and run stockfish at multipv 10 to find non-losing move that generates the said tactics.</p>
  <p>Note the code is not optimized.</p>
  <h1>Code</h1>
  <pre><code>&quot;&quot;&quot;Find pin, fork, double check and underminig moves.
  
  requirements:
      pip install chess
  &quot;&quot;&quot;
  
  
  import chess
  import chess.pgn
  import chess.engine
  
  
  def detect_forks(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      &quot;&quot;&quot;Find move the forks 2 or more pieces.
  
      Args:
          init_epd: The position in the game.
          curmove: The move from stockfish from multipv analysis.
          currank: The rank of the move from multipv analysis.
          board: The board after executing the curmove.
          wp: White player in the game.
          bp: Black player in the game.
          event: The event name.
          date: The date the event is held.
  
      Returns:
          A list of positions with fork.
      &quot;&quot;&quot;
      forks = []
  
      # The to_square of the move that forks must not be attacked by enemy.
      move_attackers = board.attackers(board.turn, curmove.to_square)
      if len(move_attackers):
          return forks
  
      prev_board = chess.Board(init_epd)
  
      # Find the attacked squares from the move.
      attacked_sq = board.attacks(curmove.to_square)
      legal_attacked_sq = []
  
      # Number of attacked squares must be 2 or more.
      if len(attacked_sq) &gt;= 2:
          # Scan all the squares and pieces on the board.
          for square, piece in board.piece_map().items():
              # If this square is not in the attacked squares, skip.
              if square not in attacked_sq:
                  continue
  
              # If the color of this square is not an enemy color, skip.
              if piece.color != board.turn:
                  continue
  
              # The attacked square should not be defended.
              defenders = board.attackers(board.turn, square)
  
              # If there is defender, skip.
              if len(defenders):
                  continue
  
              # Store the legal attacked square.
              legal_attacked_sq.append(square)
  
          # Check if all the legal attacked squares are two or more.
          if len(legal_attacked_sq) &gt;= 2:
              forks.append([init_epd, prev_board.san(curmove), currank, curscore, &quot;fork&quot;])
              forked_sq = []
              for s in legal_attacked_sq:
                  forked_sq.append(chess.square_name(s))
              print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, forked_sq: {forked_sq}, white: &quot;{wp}&quot;, black: &quot;{bp}&quot;, event: &quot;{event}&quot;, date: {date}')
  
      return forks
  
  
  def double_check(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      &quot;&quot;&quot;Find move that attacks the enemy king twice.&quot;&quot;&quot;
      dchecks = []
  
      prev_board = chess.Board(init_epd)
  
      # If the to_square of curmove does not check, skip.
      if board.is_check() and len(board.attackers(not board.turn, board.king(board.turn))) &gt; 1:
          dchecks.append([init_epd, prev_board.san(curmove), currank, curscore, &quot;double check&quot;])
          print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, checked_sq: {board.king(board.turn)}, white: &quot;{wp}&quot;, black: &quot;{bp}&quot;, event: &quot;{event}&quot;, date: {date}')
  
      return dchecks
  
  
  def undermine(init_epd, curmove, currank, curscore, board, pv, wp, bp, event, date):
      &quot;&quot;&quot;
      Check if curmove is an undermining move.
  
      curmove, oppmove, oppmovereply ------ pv sequence
      The curmove captures the enemy piece.
      oppmove captures the piece from curmove
      The piece captured by curmove is a defender_pc.
      oppmovereply captures a piece that is defended by defender_pc.
      &quot;&quot;&quot;
      undermines = []
  
      if len(pv) &lt; 3:
          return undermines
      
      pv = pv[:3]  # starts from init_epd
      
      prev_board = chess.Board(init_epd)
  
      is_capture = prev_board.is_capture(curmove)
      if not is_capture:
          return undermines
      
      # Walk the pv starting from prev_board.
      bt1 = prev_board.copy()
      b1 = bt1.copy()
      m1 = curmove
  
      bt1.push(m1)
      b2 = bt1.copy()
      m2 = pv[1]
  
      bt1.push(m2)
      b3 = bt1.copy()
      m3 = pv[2]
  
      # The last move should be a capture.
      if not b3.is_capture(m3):
          return undermines
  
      if m1.to_square == m2.to_square:
          # The piece captured by m1 is a defender.
          defended_sq = b1.attacks(m1.to_square)
          if len(defended_sq):
              # Loop thru the defended sq, if one of it is captured by m3 then m1 is an undermining move.
              for s in defended_sq:
                  if m3.to_square == s and m3.to_square != m1.to_square:
                      pv_san = b1.variation_san(pv)
                      undermines.append([init_epd, prev_board.san(curmove), currank, curscore, &quot;undermine&quot;])
                      print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, pv: {pv_san}, rank: {currank}, score: {curscore}, undermined_sq: {chess.square_name(m3.to_square)}, white: &quot;{wp}&quot;, black: &quot;{bp}&quot;, event: &quot;{event}&quot;, date: {date}')
                      return undermines
                  
      return undermines
  
  
  def detect_good_pins(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      pins = []
      prev_board = chess.Board(init_epd)
  
      # Loop thru the squares and pieces on the current board.
      for square, piece in board.piece_map().items():
  
          # Detect a pinned piece of the side to move.
          # The pinned piece is not a king and not a pawn.
          if (piece.color == board.turn and
                  piece.piece_type != chess.KING and
                  piece.piece_type != chess.PAWN):
              
              # If this square is pinned by the side not to move.
              if board.is_pinned(board.turn, square):
  
                  # Get the piece type that currently occupies the square, if this piece has no
                  # legal move, then it is pinned.
                  is_pin = True
                  for move in board.legal_moves:
                      if move.from_square == square:
                          is_pin = False
                          break
  
                  if is_pin:
  
                      # Is the last move pins the opponent?
                      # Takeback the move and check if the square is not pinned.
                      # If it is pinned, the move is not the cause of the pin.
                      # If it is not pinned, the move is the cause of the pin.
                      tmp_board = board.copy()
                      tmp_board.pop()
  
                      if not tmp_board.is_pinned(not tmp_board.turn, square):
  
                          # Save the info.
                          attackers = board.attackers(not board.turn, square)
                          defenders = board.attackers(board.turn, square)
                          if len(attackers) &gt;= len(defenders):
                              pins.append([init_epd, prev_board.san(curmove), currank, curscore, &quot;pin&quot;])
                              print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, pinned_sq: {chess.square_name(square)}, white: &quot;{wp}&quot;, black: &quot;{bp}&quot;, event: &quot;{event}&quot;, date: {date}')
  
      return pins
  
  
  if __name__ == '__main__':
      fn = 'f:/project/pgn/tatamast23.pgn'
      fn = 'f:/project/pgn/2023-pro-chess-league-main-event-regular-season.pgn'
      gcnt = 0
      all_pins = []
      all_forks = []
      all_double_check = []
      all_undermines = []
      multipv = 10
  
      engine = chess.engine.SimpleEngine.popen_uci(
          r&quot;F:\Chess\Engines\stockfish\stockfish_15.1_win_x64_popcnt\stockfish-windows-2022-x86-64-modern.exe&quot;
      )
      engine.configure({'Hash': 128})
  
      with open(fn) as pgn:
          while True:
              game = chess.pgn.read_game(pgn)
              if game is None:
                  break
  
              gcnt += 1
              print(f'game {gcnt}')
  
              wp = game.headers['White']
              bp = game.headers['Black']
              event = game.headers['Event']
              date = game.headers['Date']
  
              for node in game.mainline():
                  board = node.parent.board()
  
                  if board.ply() &lt; 24:
                      continue
  
                  # Analyze this board with the engine. Use multipv 10 to
                  # increase the chance of detecting a move that generates
                  # a dangerous pin.
                  info = engine.analyse(board, chess.engine.Limit(time=0.5), multipv=multipv)
  
                  for i in range(min(multipv, board.legal_moves.count())):
                      score = info[i]['score'].relative.score(mate_score=32000)
                      pv = info[i]['pv']
                      move = pv[0]
  
                      # The move score should not be lossing.
                      if score &lt;= -200:
                          break
  
                      # Make the engine move on the board and check if this move pins an enemy piece.
                      tmp_board = board.copy()
                      tmp_board.push(move)
  
                      pins = detect_good_pins(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(pins):
                          all_pins.append(pins)
  
                      forks = detect_forks(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(forks):
                          all_forks.append(forks)
  
                      double_checks = double_check(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(double_checks):
                          all_double_check.append(double_checks)
  
                      undermines = undermine(board.epd(), move, i+1, score, tmp_board, pv, wp, bp, event, date)
                      if len(undermines):
                          all_undermines.append(undermines)
  
              if gcnt &gt;= 1000000:
                  break
  
      engine.quit()
  
      print('pins:')
      for pins in all_pins:
          for p in pins:
              print(p)
  
      print('forks:')
      for forks in all_forks:
          for f in forks:
              print(f)
  
      print('double checks:')
      for dc in all_double_check:
          for d in dc:
              print(d)
  
      print('undermines:')
      for un in all_undermines:
          for u in un:
              print(u)
  </code></pre>
  <h2>Sample pins</h2>
  <pre><code>initepd: 3n2k1/p4r1p/1pR1p1p1/5q2/3P4/4QP2/P3N1P1/6K1 w - -, move: Rc8, rank: 2, score: 3, pinned_sq: d8, white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qc7, rank: 1, score: 0, pinned_sq: f7, white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qb7, rank: 2, score: -2, pinned_sq: f7, white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qd7, rank: 4, score: -39, pinned_sq: f7, white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14
  initepd: 8/Q4nk1/1p4p1/3p1q2/3P3p/5P2/P3N1P1/5K2 b - -, move: Qd3, rank: 7, score: -26, pinned_sq: e2, white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14
  </code></pre>
  <p>The rank is the ranking of move based from stockfish. Rank 1 is the best move based from a given analysis time of 0.5 sec.</p>
  <p>The first output line is this.</p>
  <p><a href="https://i.sstatic.net/We41b.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/We41b.png" alt="enter image description here" /></a></p>
  <p>The move that pins the enemy is Rc8. This is only ranked at number 2 (I have not recorded the top 1 engine move and the actual game move, but this is possible by revising the script) with a score of 3 cp from the point of view of the side to move.</p>
  <p>The pinned square is d8. The game is between white: &quot;Praggnanandhaa, R&quot;, black: &quot;Erigaisi, Arjun&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.14.</p>
  <p>Another line:</p>
  <pre><code>initepd: 6k1/1p3p2/1R3p1p/3pn3/8/2P1P3/r4PPP/1B2K3 b - -, move: Ra1, rank: 3, score: -108, pinned_sq: b1, white: &quot;Abdusattorov, Nodirbek&quot;, black: &quot;Caruana, Fabiano&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.15
  </code></pre>
  <p><a href="https://i.sstatic.net/1tKya.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/1tKya.png" alt="enter image description here" /></a></p>
  <h2>Sample fork output</h2>
  <pre><code>initepd: 1k1r3r/ppqb1pQp/2n5/1B6/8/B3PN2/P4PPP/2R3K1 b - -, move: Qa5, rank: 3, score: -28, forked_sq: ['b5', 'a3'], white: &quot;Carlsen, Magnus&quot;, black: &quot;Abdusattorov, Nodirbek&quot;, event: &quot;85th Tata Steel Masters&quot;, date: 2023.01.19
  </code></pre>
  <p><a href="https://i.sstatic.net/xFkrA.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/xFkrA.png" alt="enter image description here" /></a></p>
  <p>The move Qa5 will forked a3 and b5.</p>
  <h2>Sample double check</h2>
  <pre><code>initepd: 5rk1/5pp1/8/7p/1QB2P2/1P2q2P/2P2nP1/4R1K1 b - -, move: Nxh3+, rank: 1, score: 0, checked_sq: 6, white: &quot;Pranesh, M&quot;, black: &quot;Dubov, Daniil&quot;, event: &quot;Pro Chess League Main Event 2023 Regular Season&quot;, date: 2023.03.01
  </code></pre>
  <p><a href="https://i.sstatic.net/ylWAN.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/ylWAN.png" alt="enter image description here" /></a></p>
  <p>The move Nxh3 generates a double attacks to the white king.</p>
  <h2>Sample undermine 1</h2>
  <pre><code>initepd: 5rk1/2q1pp1p/pnPp1bp1/1p1B4/2r2P2/P1N1Q2P/1PP3P1/3RR1K1 b - -, move: Bxc3, pv: 1...Bxc3 2. bxc3 Nxd5, rank: 1, score: 282, undermined_sq: d5, white: &quot;Foisor, Sabina-Francesca&quot;, black: &quot;Deac, Bogdan-Daniel&quot;, event: &quot;Pro Chess League Main Event 2023 Regular Season&quot;, date: 2023.02.17
  </code></pre>
  <p><a href="https://i.sstatic.net/vPvR9.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/vPvR9.png" alt="enter image description here" /></a></p>
  <pre><code>pv: 1...Bxc3 2. bxc3 Nxd5, rank: 1, score: 282, undermined_sq: d5
  </code></pre>
  <h2>Sample undermine 2</h2>
  <pre><code>initepd: 5rk1/2q2pp1/R1rp1n2/7p/1pBQ1P2/1P5P/2P3P1/3R2K1 w - -, move: Rxc6, pv: 1. Rxc6 Qxc6 2. Qxd6, rank: 1, score: 174, undermined_sq: d6, white: &quot;Pranesh, M&quot;, black: &quot;Dubov, Daniil&quot;, event: &quot;Pro Chess League Main Event 2023 Regular Season&quot;, date: 2023.03.01
  </code></pre>
  <p><a href="https://i.sstatic.net/MYSGR.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/MYSGR.png" alt="enter image description here" /></a></p>
  <pre><code>pv: 1. Rxc6 Qxc6 2. Qxd6, rank: 1, score: 174, undermined_sq: d6
  </code></pre>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-16T20:02:35.983Z
Answers.Text: Very cool! This is pretty inline with what I might code in the future. It isn't perfect, because it only detects pieces that literally cannot move (so it won't detect a bishop pinning a queen, for example), but that would take a bit more coding so as to prevent stupid blunders (the queen could just take the bishop). Still pretty awesome though!
Answers.UserDisplayName: Lukas Kawalec
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-17T01:19:56.373Z
Answers.Text: I intentionally exclude the "partial pin" such as bishop pins queen. The code can be easily revised to include partial pins.
Answers.UserDisplayName: ferdy
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-17T14:05:46.340Z
Answers.Text: Revised code to also detect forks, double-check and undermine.
Answers.UserDisplayName: ferdy
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-16T15:02:16.733Z
Answers.LastActivityDate: 2023-03-17T14:08:47.190Z
Answers.LastEditDate: 2023-03-17T14:08:47.190Z
Answers.UserDisplayName: ferdy
Answers.Body: <p>If you use Stockfish via some sort of GUI, like Scid, Scid vs PC, Fritz or Chessbase, as most people do, then your GUI will have an option to show not just the best sequence of moves but the best n such. I normally have it set to 2 or 3.</p>
  <p>If there are at least this number of legal moves then it will still show this number of alternatives even if only one move (say a queen recapture) is good and the other moves are blunders. If there is only one legal move then it just shows that one move.</p>
  <p>I don't know what limit Stockfish places on the number of lines it will give you, but whatever number that is, if your bad but tempting move is in one of those lines then you can get it to show you.</p>
  <p>If, say, Stockfish can return its best 10 lines and your bad move is number 9 then you can use this to see it. If your bad move is number 11 or worse then not.</p>
  <blockquote>
  <p>the opponent has a fork which loses the game - UNLESS you play an absolutely perfect sequence of 6, hard to find moves</p>
  </blockquote>
  <p>In general this type of situation could be detected by setting the maximum depth for evaluation to a low figure.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T15:35:49.923Z
Answers.Text: It would be a useful addition for stockfish to supplement suggested moves with it's view on how difficult a move or sequence of moves is to find, then a GUI could be set to ignore sequences that are beyond the ability of the person using it. Shame, I'm not aware of such a thing existing at the moment
Answers.UserDisplayName: Darren H
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T16:06:07.950Z
Answers.Text: I am curious how hard it would be to write a script to do that.  Essentially running a bunch of stockfish instances in parallel with different max depths, and ranking moves by some algorithm that weighs how much a move's score changes at each max depth level with how deep the max search depth was.
Answers.UserDisplayName: Chuu
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T17:16:13.647Z
Answers.Text: Thanks, that's an idea! I can also code something myself with my own set of rules, it should accomplish what I am seeking, but I guess engines can't do it by themselves.
Answers.UserDisplayName: Lukas Kawalec
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T06:51:28.203Z
Answers.Text: This answer is correct. By adjusting the number of lines the chess engine shows, you may be able to discover funny lines.
Answers.UserDisplayName: Emre Bener
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T10:11:18.650Z
Answers.Text: Also, at least till version 4.22, ScidVsPC has the "Exclude move" option: in any position you can "forbid" the engine to explore certain moves. It's interesting to leave the engine analyzing for a few seconds, and once you get the best moves, add them to the "Exclude move" list. This is more efficient than just adding more moves to be showed, since the engine won't waste resources in the best moves nad will focuse in the weakest ones. A very useful feature to find interesting opening ideas (specially at amateur level).
Answers.UserDisplayName: emdio
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T10:20:30.820Z
Answers.Text: @Chuu I think this could be a great answer to this question. Running Stockfish multiple times with different Max-Depth should solve this quite nicely. Moves on which they agree are "easy" and moves which only the engine with higher depth find are "hard"
Answers.UserDisplayName: Falco
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T10:26:23.003Z
Answers.LastActivityDate: 2023-03-14T11:48:57.703Z
Answers.LastEditDate: 2023-03-14T11:48:57.703Z
Answers.UserDisplayName: Brian Towers
Answers.Body: <p>No. Stockfish is designed to always assume optimal play, so it treats all the moves it finds &quot;equally&quot; without any consideration for difficulty. Keep in mind that engines are a tool to assist analysis, not a replacement for it. It gives you the answers, but you're still the one posing the questions.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T17:17:22.997Z
Answers.Text: I guess you are right. It's obviously possible to make additional code or alter the engine  in order to find these moves, because how else would Lichess be able to have puzzle themes with 200k+ entries unless an algorithm identified a move as a "pin" or "fork". I am thinking about coding this myself though, it shouldn't be too hard
Answers.UserDisplayName: Lukas Kawalec
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T06:50:13.630Z
Answers.Text: You are simply wrong. OP is asking if it is possible to see alternative lines, even if they are not as good as the best line, which is possible with every chess engine.
Answers.UserDisplayName: Emre Bener
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T10:00:01.930Z
Answers.Text: @Mephisto that's not what OP is asking for. By showing multiple lines you're still the one who has to dig into them and check which ones require more precise play than others.
Answers.UserDisplayName: David
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T13:23:25.060Z
Answers.Text: the question is; "Is there a way to use Stockfish to see tactics that aren't necessarily great?". the answer is; yes, it is possible, as viewing multiple lines could reveal tactics that aren't necessarily great.
Answers.UserDisplayName: Emre Bener
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T15:28:12.500Z
Answers.Text: @LukasKawalec: It's not complicated to find puzzles. A puzzle is a sequence of forced moves, where a move is "forced" if all other possible moves are much worse. You can figure that out just by looking at the numerical evaluation (after [adjusting for centipawns being a poor metric](https://lichess.org/page/accuracy)). Finding *good* puzzles, or puzzles that effectively train a specific concept, is harder, of course.
Answers.UserDisplayName: Kevin
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-15T08:55:46.903Z
Answers.Text: @Mephisto by showing multiple lines you don't see which ones involve any interesting tactics (and you'll only see a fixed number of them). Failing tactics are rarely 2nd or 3rd engine choices (they're usually blunders).
Answers.UserDisplayName: David
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-15T08:57:31.603Z
Answers.Text: @Kevin it's more complicated. Puzzles are supposed to involve some sort of tactical motif and difficulty from a human perspective. If you just stick to "forcedness" according to an engine, you'll get a very poor collection of puzzles.
Answers.UserDisplayName: David
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-15T16:12:33.063Z
Answers.Text: @David: There are puzzles that are supposed to be beautiful, and there are puzzles that are supposed to drill tactics (i.e. the kind you see on http://lichess.org/training). I was talking about the latter, because that's what I assumed OP was referencing.
Answers.UserDisplayName: Kevin
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-15T17:14:03.550Z
Answers.Text: @Kevin lichess does much more than running positions through engines showing multiple lines to decide where to create a puzzle. In fact their puzzles had a bad reputation in the beginning when the algorythm they used was much simpler.
Answers.UserDisplayName: David
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-13T07:50:55.233Z
Answers.LastActivityDate: 2023-03-13T07:50:55.233Z
Answers.UserDisplayName: David
Answers.Body: <p>Using the &quot;top n lines&quot; approach, one way of finding tactics that &quot;almost&quot; work is to look for moves that are at the same time:</p>
  <ol>
  <li>Sacrifice material (a light officer or more)</li>
  <li>Get a flat 0.00 evaluation from the engine.</li>
  </ol>
  <p>These kind of moves should be interesting to look closer at, since giving up material should normally mean you are losing. Yet, the machine says you aren't. That means there must be some concrete tactic that works out for you in the future.</p>
  <p>Ideally, what this reveals is an attack with you winning in almost every line except for a very accurate defence where the opponent holds the draw. Or the reverse situation, of course, with you defending.</p>
  <p>In general, positions with unbalanced material that are still drawn with perfect play should be a perfect example of &quot;tactics that aren't necessarily great&quot;. The engine helps you with the &quot;still drawn with perfect play&quot; identification part.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T17:20:42.430Z
Answers.Text: I think this the closest answer so far, thanks. What parameter are you talking about for sacrificing material? "Light officer"?
Answers.UserDisplayName: Lukas Kawalec
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T11:43:00.903Z
Answers.LastActivityDate: 2023-03-14T11:43:00.903Z
Answers.UserDisplayName: SE - stop firing the good guys
Answers.Body: <p>I found something pretty cool, which I guess doesn't answer my question, but it's pretty close in nature:</p>
  <p>Lichess has a &quot;Boris Trapsky&quot; bot, in which it makes blunders, or sacs pieces that, if taken, lead to a major advantage, or mate. Doing this against an engine would of course be pointless as with correct play it's just a stupid blunder.</p>
  <p>I guess in a perfect world you could somehow use this bot to analyze your own games and find those kinds of moves, which is pretty much what I'm looking for. So in the end, I think it is possible, but requires additional coding, and nothing offers this feature out of the box.</p>
  <p>Here's the link: <a href="https://lichess.org/study/qDFa6puH" rel="nofollow noreferrer">https://lichess.org/study/qDFa6puH</a></p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-03-14T17:27:34.933Z
Answers.LastActivityDate: 2023-03-14T17:27:34.933Z
Answers.UserDisplayName: Lukas Kawalec
Body: <p>Basically - I want to utilize an engine to see tactics through the whole game, regardless of whether they're a good move or not. Sometimes, Stockfish may say a position is absolutely winning, but the opponent has a fork which loses the game - UNLESS you play an absolutely perfect sequence of 6, hard to find moves. So, it doesn't even tell you that there was a fork - because the engine assumes you play perfectly, so therefore you are absolutely winning. But, to beginners, it would not be so.</p>
  <p>Is there some way to do this, using Stockfish or other?</p>
  <hr />
  <p><strong>Edit:</strong> Basically - it would be really cool to &quot;filter&quot; moves by a theme - pin, fork, discovered attack, etc. I think this is really the essence of my question. And then, theoretically, you could go beyond that make the engine have low depth, more lines, etc. But simply increasing the amount of lines it shows you isn't exactly what I'm looking for. (Sorry for my badly phrased question.)</p>
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-03-15T02:13:57.183Z
Comments.Text: https://chesstricksapp.wordpress.com/ This software may have what you are looking for. It is obviously not perfect, but it can find some nasty tricks
Comments.UserDisplayName: Pinak Paliwal
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-03-15T02:45:55.783Z
Comments.Text: @chess_lover_6 that's pretty cool, but when comparing it to the Boris Trapsky bot, it doesn't find the moves it makes... I think that's because it's trying to find moves that only have one correct answer, but most moves will have multiple, so it doesn't consider it... but, cool software anyway - it's pretty close
Comments.UserDisplayName: Lukas Kawalec
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-03-16T04:03:06.467Z
Comments.Text: Programmatically finding positions that has a tactical move is easier to do than classifying the position into different tactical themes. There are solutions to these issues. First list and understand each tactical themes. You can use python-chess library and engine to detect these themes. The other way is to train a model that can classify the position.
Comments.UserDisplayName: ferdy
Tags: <engines><tactics><stockfish>
Title: Is there a way to use Stockfish to see tactics that aren't necessarily great?
ThreadText: # Is there a way to use Stockfish to see tactics that aren't necessarily great?
  
  (Asked by: username_0 on 2023-03-13)
  
  Basically - I want to utilize an engine to see tactics through the whole game, regardless of whether they're a good move or not. Sometimes, Stockfish may say a position is absolutely winning, but the opponent has a fork which loses the game - UNLESS you play an absolutely perfect sequence of 6, hard to find moves. So, it doesn't even tell you that there was a fork - because the engine assumes you play perfectly, so therefore you are absolutely winning. But, to beginners, it would not be so.
  
  Is there some way to do this, using Stockfish or other?
  
  ---
  
  **Edit:** Basically - it would be really cool to "filter" moves by a theme - pin, fork, discovered attack, etc. I think this is really the essence of my question. And then, theoretically, you could go beyond that make the engine have low depth, more lines, etc. But simply increasing the amount of lines it shows you isn't exactly what I'm looking for. (Sorry for my badly phrased question.)
  
  **Tags:** engines, tactics, stockfish
  
  > **Comments:**
  > **username_1**: https://chesstricksapp.wordpress.com/ This software may have what you are looking for. It is obviously not perfect, but it can find some nasty tricks  
  > **username_0**: @chess_lover_6 that's pretty cool, but when comparing it to the Boris Trapsky bot, it doesn't find the moves it makes... I think that's because it's trying to find moves that only have one correct answer, but most moves will have multiple, so it doesn't consider it... but, cool software anyway - it's pretty close  
  > **username_2**: Programmatically finding positions that has a tactical move is easier to do than classifying the position into different tactical themes. There are solutions to these issues. First list and understand each tactical themes. You can use python-chess library and engine to detect these themes. The other way is to train a model that can classify the position.  
  
  ---
  
  ## 5 answers
  
  ### Answer by username_2 on 2023-03-16. Score: 5 | Accepted answer
  
  Created a script that will detect the following tactical themes.
  
  pins
  forks
  double check
  undermine
  
  It will read the games in the pgn file. Visit each position in the game and run stockfish at multipv 10 to find non-losing move that generates the said tactics.
  Note the code is not optimized.
  Code
  """Find pin, fork, double check and underminig moves.
  
  requirements:
      pip install chess
  """
  
  
  import chess
  import chess.pgn
  import chess.engine
  
  
  def detect_forks(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      """Find move the forks 2 or more pieces.
  
      Args:
          init_epd: The position in the game.
          curmove: The move from stockfish from multipv analysis.
          currank: The rank of the move from multipv analysis.
          board: The board after executing the curmove.
          wp: White player in the game.
          bp: Black player in the game.
          event: The event name.
          date: The date the event is held.
  
      Returns:
          A list of positions with fork.
      """
      forks = []
  
      # The to_square of the move that forks must not be attacked by enemy.
      move_attackers = board.attackers(board.turn, curmove.to_square)
      if len(move_attackers):
          return forks
  
      prev_board = chess.Board(init_epd)
  
      # Find the attacked squares from the move.
      attacked_sq = board.attacks(curmove.to_square)
      legal_attacked_sq = []
  
      # Number of attacked squares must be 2 or more.
      if len(attacked_sq) >= 2:
          # Scan all the squares and pieces on the board.
          for square, piece in board.piece_map().items():
              # If this square is not in the attacked squares, skip.
              if square not in attacked_sq:
                  continue
  
              # If the color of this square is not an enemy color, skip.
              if piece.color != board.turn:
                  continue
  
              # The attacked square should not be defended.
              defenders = board.attackers(board.turn, square)
  
              # If there is defender, skip.
              if len(defenders):
                  continue
  
              # Store the legal attacked square.
              legal_attacked_sq.append(square)
  
          # Check if all the legal attacked squares are two or more.
          if len(legal_attacked_sq) >= 2:
              forks.append([init_epd, prev_board.san(curmove), currank, curscore, "fork"])
              forked_sq = []
              for s in legal_attacked_sq:
                  forked_sq.append(chess.square_name(s))
              print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, forked_sq: {forked_sq}, white: "{wp}", black: "{bp}", event: "{event}", date: {date}')
  
      return forks
  
  
  def double_check(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      """Find move that attacks the enemy king twice."""
      dchecks = []
  
      prev_board = chess.Board(init_epd)
  
      # If the to_square of curmove does not check, skip.
      if board.is_check() and len(board.attackers(not board.turn, board.king(board.turn))) > 1:
          dchecks.append([init_epd, prev_board.san(curmove), currank, curscore, "double check"])
          print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, checked_sq: {board.king(board.turn)}, white: "{wp}", black: "{bp}", event: "{event}", date: {date}')
  
      return dchecks
  
  
  def undermine(init_epd, curmove, currank, curscore, board, pv, wp, bp, event, date):
      """
      Check if curmove is an undermining move.
  
      curmove, oppmove, oppmovereply ------ pv sequence
      The curmove captures the enemy piece.
      oppmove captures the piece from curmove
      The piece captured by curmove is a defender_pc.
      oppmovereply captures a piece that is defended by defender_pc.
      """
      undermines = []
  
      if len(pv) < 3:
          return undermines
      
      pv = pv[:3]  # starts from init_epd
      
      prev_board = chess.Board(init_epd)
  
      is_capture = prev_board.is_capture(curmove)
      if not is_capture:
          return undermines
      
      # Walk the pv starting from prev_board.
      bt1 = prev_board.copy()
      b1 = bt1.copy()
      m1 = curmove
  
      bt1.push(m1)
      b2 = bt1.copy()
      m2 = pv[1]
  
      bt1.push(m2)
      b3 = bt1.copy()
      m3 = pv[2]
  
      # The last move should be a capture.
      if not b3.is_capture(m3):
          return undermines
  
      if m1.to_square == m2.to_square:
          # The piece captured by m1 is a defender.
          defended_sq = b1.attacks(m1.to_square)
          if len(defended_sq):
              # Loop thru the defended sq, if one of it is captured by m3 then m1 is an undermining move.
              for s in defended_sq:
                  if m3.to_square == s and m3.to_square != m1.to_square:
                      pv_san = b1.variation_san(pv)
                      undermines.append([init_epd, prev_board.san(curmove), currank, curscore, "undermine"])
                      print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, pv: {pv_san}, rank: {currank}, score: {curscore}, undermined_sq: {chess.square_name(m3.to_square)}, white: "{wp}", black: "{bp}", event: "{event}", date: {date}')
                      return undermines
                  
      return undermines
  
  
  def detect_good_pins(init_epd, curmove, currank, curscore, board, wp, bp, event, date):
      pins = []
      prev_board = chess.Board(init_epd)
  
      # Loop thru the squares and pieces on the current board.
      for square, piece in board.piece_map().items():
  
          # Detect a pinned piece of the side to move.
          # The pinned piece is not a king and not a pawn.
          if (piece.color == board.turn and
                  piece.piece_type != chess.KING and
                  piece.piece_type != chess.PAWN):
              
              # If this square is pinned by the side not to move.
              if board.is_pinned(board.turn, square):
  
                  # Get the piece type that currently occupies the square, if this piece has no
                  # legal move, then it is pinned.
                  is_pin = True
                  for move in board.legal_moves:
                      if move.from_square == square:
                          is_pin = False
                          break
  
                  if is_pin:
  
                      # Is the last move pins the opponent?
                      # Takeback the move and check if the square is not pinned.
                      # If it is pinned, the move is not the cause of the pin.
                      # If it is not pinned, the move is the cause of the pin.
                      tmp_board = board.copy()
                      tmp_board.pop()
  
                      if not tmp_board.is_pinned(not tmp_board.turn, square):
  
                          # Save the info.
                          attackers = board.attackers(not board.turn, square)
                          defenders = board.attackers(board.turn, square)
                          if len(attackers) >= len(defenders):
                              pins.append([init_epd, prev_board.san(curmove), currank, curscore, "pin"])
                              print(f'initepd: {init_epd}, move: {prev_board.san(curmove)}, rank: {currank}, score: {curscore}, pinned_sq: {chess.square_name(square)}, white: "{wp}", black: "{bp}", event: "{event}", date: {date}')
  
      return pins
  
  
  if __name__ == '__main__':
      fn = 'f:/project/pgn/tatamast23.pgn'
      fn = 'f:/project/pgn/2023-pro-chess-league-main-event-regular-season.pgn'
      gcnt = 0
      all_pins = []
      all_forks = []
      all_double_check = []
      all_undermines = []
      multipv = 10
  
      engine = chess.engine.SimpleEngine.popen_uci(
          r"F:\Chess\Engines\stockfish\stockfish_15.1_win_x64_popcnt\stockfish-windows-2022-x86-64-modern.exe"
      )
      engine.configure({'Hash': 128})
  
      with open(fn) as pgn:
          while True:
              game = chess.pgn.read_game(pgn)
              if game is None:
                  break
  
              gcnt += 1
              print(f'game {gcnt}')
  
              wp = game.headers['White']
              bp = game.headers['Black']
              event = game.headers['Event']
              date = game.headers['Date']
  
              for node in game.mainline():
                  board = node.parent.board()
  
                  if board.ply() < 24:
                      continue
  
                  # Analyze this board with the engine. Use multipv 10 to
                  # increase the chance of detecting a move that generates
                  # a dangerous pin.
                  info = engine.analyse(board, chess.engine.Limit(time=0.5), multipv=multipv)
  
                  for i in range(min(multipv, board.legal_moves.count())):
                      score = info[i]['score'].relative.score(mate_score=32000)
                      pv = info[i]['pv']
                      move = pv[0]
  
                      # The move score should not be lossing.
                      if score <= -200:
                          break
  
                      # Make the engine move on the board and check if this move pins an enemy piece.
                      tmp_board = board.copy()
                      tmp_board.push(move)
  
                      pins = detect_good_pins(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(pins):
                          all_pins.append(pins)
  
                      forks = detect_forks(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(forks):
                          all_forks.append(forks)
  
                      double_checks = double_check(board.epd(), move, i+1, score, tmp_board, wp, bp, event, date)
                      if len(double_checks):
                          all_double_check.append(double_checks)
  
                      undermines = undermine(board.epd(), move, i+1, score, tmp_board, pv, wp, bp, event, date)
                      if len(undermines):
                          all_undermines.append(undermines)
  
              if gcnt >= 1000000:
                  break
  
      engine.quit()
  
      print('pins:')
      for pins in all_pins:
          for p in pins:
              print(p)
  
      print('forks:')
      for forks in all_forks:
          for f in forks:
              print(f)
  
      print('double checks:')
      for dc in all_double_check:
          for d in dc:
              print(d)
  
      print('undermines:')
      for un in all_undermines:
          for u in un:
              print(u)
  
  Sample pins
  initepd: 3n2k1/p4r1p/1pR1p1p1/5q2/3P4/4QP2/P3N1P1/6K1 w - -, move: Rc8, rank: 2, score: 3, pinned_sq: d8, white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qc7, rank: 1, score: 0, pinned_sq: f7, white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qb7, rank: 2, score: -2, pinned_sq: f7, white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14
  initepd: 2Q5/p4nk1/1p4p1/3p2q1/3P3p/5P2/P3N1P1/5K2 w - -, move: Qd7, rank: 4, score: -39, pinned_sq: f7, white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14
  initepd: 8/Q4nk1/1p4p1/3p1q2/3P3p/5P2/P3N1P1/5K2 b - -, move: Qd3, rank: 7, score: -26, pinned_sq: e2, white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14
  
  The rank is the ranking of move based from stockfish. Rank 1 is the best move based from a given analysis time of 0.5 sec.
  The first output line is this.
  
  The move that pins the enemy is Rc8. This is only ranked at number 2 (I have not recorded the top 1 engine move and the actual game move, but this is possible by revising the script) with a score of 3 cp from the point of view of the side to move.
  The pinned square is d8. The game is between white: "Praggnanandhaa, R", black: "Erigaisi, Arjun", event: "85th Tata Steel Masters", date: 2023.01.14.
  Another line:
  initepd: 6k1/1p3p2/1R3p1p/3pn3/8/2P1P3/r4PPP/1B2K3 b - -, move: Ra1, rank: 3, score: -108, pinned_sq: b1, white: "Abdusattorov, Nodirbek", black: "Caruana, Fabiano", event: "85th Tata Steel Masters", date: 2023.01.15
  
  
  Sample fork output
  initepd: 1k1r3r/ppqb1pQp/2n5/1B6/8/B3PN2/P4PPP/2R3K1 b - -, move: Qa5, rank: 3, score: -28, forked_sq: ['b5', 'a3'], white: "Carlsen, Magnus", black: "Abdusattorov, Nodirbek", event: "85th Tata Steel Masters", date: 2023.01.19
  
  
  The move Qa5 will forked a3 and b5.
  Sample double check
  initepd: 5rk1/5pp1/8/7p/1QB2P2/1P2q2P/2P2nP1/4R1K1 b - -, move: Nxh3+, rank: 1, score: 0, checked_sq: 6, white: "Pranesh, M", black: "Dubov, Daniil", event: "Pro Chess League Main Event 2023 Regular Season", date: 2023.03.01
  
  
  The move Nxh3 generates a double attacks to the white king.
  Sample undermine 1
  initepd: 5rk1/2q1pp1p/pnPp1bp1/1p1B4/2r2P2/P1N1Q2P/1PP3P1/3RR1K1 b - -, move: Bxc3, pv: 1...Bxc3 2. bxc3 Nxd5, rank: 1, score: 282, undermined_sq: d5, white: "Foisor, Sabina-Francesca", black: "Deac, Bogdan-Daniel", event: "Pro Chess League Main Event 2023 Regular Season", date: 2023.02.17
  
  
  pv: 1...Bxc3 2. bxc3 Nxd5, rank: 1, score: 282, undermined_sq: d5
  
  Sample undermine 2
  initepd: 5rk1/2q2pp1/R1rp1n2/7p/1pBQ1P2/1P5P/2P3P1/3R2K1 w - -, move: Rxc6, pv: 1. Rxc6 Qxc6 2. Qxd6, rank: 1, score: 174, undermined_sq: d6, white: "Pranesh, M", black: "Dubov, Daniil", event: "Pro Chess League Main Event 2023 Regular Season", date: 2023.03.01
  
  
  pv: 1. Rxc6 Qxc6 2. Qxd6, rank: 1, score: 174, undermined_sq: d6
  
  > **Comments:**
  > **username_0**: Very cool! This is pretty inline with what I might code in the future. It isn't perfect, because it only detects pieces that literally cannot move (so it won't detect a bishop pinning a queen, for example), but that would take a bit more coding so as to prevent stupid blunders (the queen could just take the bishop). Still pretty awesome though!  
  > **username_2**: I intentionally exclude the "partial pin" such as bishop pins queen. The code can be easily revised to include partial pins.  
  > **username_2**: Revised code to also detect forks, double-check and undermine.  
  
  ---
  
  ### Answer by username_3 on 2023-03-13. Score: 10
  
  If you use Stockfish via some sort of GUI, like Scid, Scid vs PC, Fritz or Chessbase, as most people do, then your GUI will have an option to show not just the best sequence of moves but the best n such. I normally have it set to 2 or 3.
  
  If there are at least this number of legal moves then it will still show this number of alternatives even if only one move (say a queen recapture) is good and the other moves are blunders. If there is only one legal move then it just shows that one move.
  
  I don't know what limit Stockfish places on the number of lines it will give you, but whatever number that is, if your bad but tempting move is in one of those lines then you can get it to show you.
  
  If, say, Stockfish can return its best 10 lines and your bad move is number 9 then you can use this to see it. If your bad move is number 11 or worse then not.
  
  > the opponent has a fork which loses the game - UNLESS you play an absolutely perfect sequence of 6, hard to find moves
  
  In general this type of situation could be detected by setting the maximum depth for evaluation to a low figure.
  
  > **Comments:**
  > **username_4**: It would be a useful addition for stockfish to supplement suggested moves with it's view on how difficult a move or sequence of moves is to find, then a GUI could be set to ignore sequences that are beyond the ability of the person using it. Shame, I'm not aware of such a thing existing at the moment  
  > **username_5**: I am curious how hard it would be to write a script to do that.  Essentially running a bunch of stockfish instances in parallel with different max depths, and ranking moves by some algorithm that weighs how much a move's score changes at each max depth level with how deep the max search depth was.  
  > **username_0**: Thanks, that's an idea! I can also code something myself with my own set of rules, it should accomplish what I am seeking, but I guess engines can't do it by themselves.  
  > **username_6**: This answer is correct. By adjusting the number of lines the chess engine shows, you may be able to discover funny lines.  
  > **username_7**: Also, at least till version 4.22, ScidVsPC has the "Exclude move" option: in any position you can "forbid" the engine to explore certain moves. It's interesting to leave the engine analyzing for a few seconds, and once you get the best moves, add them to the "Exclude move" list. This is more efficient than just adding more moves to be showed, since the engine won't waste resources in the best moves nad will focuse in the weakest ones. A very useful feature to find interesting opening ideas (specially at amateur level).  
  > **username_8**: @username_5 I think this could be a great answer to this question. Running Stockfish multiple times with different Max-Depth should solve this quite nicely. Moves on which they agree are "easy" and moves which only the engine with higher depth find are "hard"  
  
  ---
  
  ### Answer by username_9 on 2023-03-13. Score: 4
  
  No. Stockfish is designed to always assume optimal play, so it treats all the moves it finds "equally" without any consideration for difficulty. Keep in mind that engines are a tool to assist analysis, not a replacement for it. It gives you the answers, but you're still the one posing the questions.
  
  > **Comments:**
  > **username_0**: I guess you are right. It's obviously possible to make additional code or alter the engine  in order to find these moves, because how else would Lichess be able to have puzzle themes with 200k+ entries unless an algorithm identified a move as a "pin" or "fork". I am thinking about coding this myself though, it shouldn't be too hard  
  > **username_6**: You are simply wrong. OP is asking if it is possible to see alternative lines, even if they are not as good as the best line, which is possible with every chess engine.  
  > **username_9**: @Mephisto that's not what OP is asking for. By showing multiple lines you're still the one who has to dig into them and check which ones require more precise play than others.  
  > **username_6**: the question is; "Is there a way to use Stockfish to see tactics that aren't necessarily great?". the answer is; yes, it is possible, as viewing multiple lines could reveal tactics that aren't necessarily great.  
  > **username_10**: @LukasKawalec: It's not complicated to find puzzles. A puzzle is a sequence of forced moves, where a move is "forced" if all other possible moves are much worse. You can figure that out just by looking at the numerical evaluation (after [adjusting for centipawns being a poor metric](https://lichess.org/page/accuracy)). Finding *good* puzzles, or puzzles that effectively train a specific concept, is harder, of course.  
  > **username_9**: @Mephisto by showing multiple lines you don't see which ones involve any interesting tactics (and you'll only see a fixed number of them). Failing tactics are rarely 2nd or 3rd engine choices (they're usually blunders).  
  > **username_9**: @username_10 it's more complicated. Puzzles are supposed to involve some sort of tactical motif and difficulty from a human perspective. If you just stick to "forcedness" according to an engine, you'll get a very poor collection of puzzles.  
  > **username_10**: @username_9: There are puzzles that are supposed to be beautiful, and there are puzzles that are supposed to drill tactics (i.e. the kind you see on http://lichess.org/training). I was talking about the latter, because that's what I assumed OP was referencing.  
  > **username_9**: @username_10 lichess does much more than running positions through engines showing multiple lines to decide where to create a puzzle. In fact their puzzles had a bad reputation in the beginning when the algorythm they used was much simpler.  
  
  ---
  
  ### Answer by username_11 on 2023-03-14. Score: 1
  
  Using the "top n lines" approach, one way of finding tactics that "almost" work is to look for moves that are at the same time:
  
  1. Sacrifice material (a light officer or more)
  2. Get a flat 0.00 evaluation from the engine.
  
  These kind of moves should be interesting to look closer at, since giving up material should normally mean you are losing. Yet, the machine says you aren't. That means there must be some concrete tactic that works out for you in the future.
  
  Ideally, what this reveals is an attack with you winning in almost every line except for a very accurate defence where the opponent holds the draw. Or the reverse situation, of course, with you defending.
  
  In general, positions with unbalanced material that are still drawn with perfect play should be a perfect example of "tactics that aren't necessarily great". The engine helps you with the "still drawn with perfect play" identification part.
  
  > **Comments:**
  > **username_0**: I think this the closest answer so far, thanks. What parameter are you talking about for sacrificing material? "Light officer"?  
  
  ---
  
  ### Answer by username_0 on 2023-03-14. Score: 0
  
  I found something pretty cool, which I guess doesn't answer my question, but it's pretty close in nature:
  
  Lichess has a "Boris Trapsky" bot, in which it makes blunders, or sacs pieces that, if taken, lead to a major advantage, or mate. Doing this against an engine would of course be pointless as with correct play it's just a stupid blunder.
  
  I guess in a perfect world you could somehow use this bot to analyze your own games and find those kinds of moves, which is pretty much what I'm looking for. So in the end, I think it is possible, but requires additional coding, and nothing offers this feature out of the box.
  
  Here's the link: <https://lichess.org/study/qDFa6puH>
  
  
  ---
  
  