15

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.)

Lukas Kawalec
  • 261
  • 2
  • 6
  • 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 – Pinak Paliwal Mar 15 '23 at 02:13
  • @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 – Lukas Kawalec Mar 15 '23 at 02:45
  • 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. – ferdy Mar 16 '23 at 04:03

5 Answers5

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.

Brian Towers
  • 96,800
  • 11
  • 239
  • 397
  • 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 – Darren H Mar 13 '23 at 15:35
  • 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. – Chuu Mar 13 '23 at 16:06
  • 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. – Lukas Kawalec Mar 13 '23 at 17:16
  • 1
    This answer is correct. By adjusting the number of lines the chess engine shows, you may be able to discover funny lines. – Emre Bener Mar 14 '23 at 06:51
  • 1
    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). – emdio Mar 14 '23 at 10:11
  • 1
    @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" – Falco Mar 14 '23 at 10:20
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.

David
  • 16,275
  • 26
  • 61
  • 1
    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 – Lukas Kawalec Mar 13 '23 at 17:17
  • 1
    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. – Emre Bener Mar 14 '23 at 06:50
  • 3
    @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. – David Mar 14 '23 at 10:00
  • 1
    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. – Emre Bener Mar 14 '23 at 13:23
  • @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). Finding good puzzles, or puzzles that effectively train a specific concept, is harder, of course. – Kevin Mar 14 '23 at 15:28
  • @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). – David Mar 15 '23 at 08:55
  • @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. – David Mar 15 '23 at 08:57
  • @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. – Kevin Mar 15 '23 at 16:12
  • @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. – David Mar 15 '23 at 17:14
4

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.

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

pv: 1. Rxc6 Qxc6 2. Qxd6, rank: 1, score: 174, undermined_sq: d6
ferdy
  • 4,015
  • 6
  • 18
  • 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! – Lukas Kawalec Mar 16 '23 at 20:02
  • I intentionally exclude the "partial pin" such as bishop pins queen. The code can be easily revised to include partial pins. – ferdy Mar 17 '23 at 01:19
  • Revised code to also detect forks, double-check and undermine. – ferdy Mar 17 '23 at 14:05
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.

  • I think this the closest answer so far, thanks. What parameter are you talking about for sacrificing material? "Light officer"? – Lukas Kawalec Mar 14 '23 at 17:20
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

Lukas Kawalec
  • 261
  • 2
  • 6