Programmatic Chess Analysis: Determining if a Player Can Castle or Not

It all started a few years ago when I got the idea of programmatically analyzing a chess position using the Stockfish chess engine to measure position “risk” or “sharpness”. In order to do that I needed a utility function to convert a chess game stored in PGN notation to FEN notation.

Implementing that conversion function took a long time and was very difficult, but a fun challenge.

In all the functions and helper functions for the file conversion utility, it was never necessary to determine if either player could legally castle to kingside or queenside because the source PGN was assumed to be valid.

One morning before work, I decided to implement functions to determine if a player can legally castle in a given chess position. I figured it would be a bit tricky and it was.

The key ideas are that a player cannot castle if it’s not his turn. A player cannot castle to a side if his king or side-rook has moved at any time. A player cannot castle out of check. A player cannot castle into check. A player cannot castle if any of the two intervening squares are attacked. All of these conditions except the first one (which player to move) are difficult to implement in code.

I implemented four separate functions: can_white_castle_kside(), can_white_castle_qside(), can_black_castle_kside(), can_black_castle_qside().

1.) To check which player’s move it is, I check token [1] of the position FEN string (“w” or “b”).

2.) To check if a player has lost, or still has, the right to castle, I check the position FEN string which has a token [2] like “KQkq” where K means white can castle kingside, Q means white can castle queenside, and so on.

3.) To check if a player is in check, I used an existing is_king_in_check(king_color, fen_string_full) function.

4.) To check if a player is attempting to castle into check, I construct the proposed position after castling and then see if the king is now in check.

5). To check if any of the two intervening squares are under attack, I use an existing helper function can_reach(piece_type_uncased, came_from_square, landing_square, board_position). For example, for the initial starting position, can_reach(“N”, 57, 42, initial_board_position) is True because the white knight on b1 can reach square c3. The can_reach() function is used to see any enemy piece can attack either of the two intervening squares..


My chess functions use the board representation on the right

The resulting code is:

  @staticmethod
  def can_white_castle_kside(fen_string_full):
    # FEN like 
    # "r1bqkb1r/1ppp1ppp/p1n2n2/4p3/B3P3/5N2/PPPP1PPP/RNBQ1RK1 w KQkq - 3 5"
    tokens = fen_string_full.split(' ')

    # check the FEN string
    if tokens[1] != "w": return False  # not white's turn
    castling_info = tokens[2]
    if castling_info.find("K") == -1:
      return False

    fen_board = tokens[0]
    board_pos = ChessFunctions.fen_string_to_board_position(fen_board)

    # is position "K _ _ R" ?
    if board_pos[60] != "K": return False
    elif board_pos[61] != "1": return False
    elif board_pos[62] != "1": return False
    elif board_pos[63] != "R": return False

    # is white king currently in check?
    if ChessFunctions.is_king_in_check("w", fen_string_full) == True:
      return False

    # attempt to castle into check?
    new_board_pos = ChessFunctions.fen_string_to_board_position(fen_board)
    new_board_pos[60] = "1"
    new_board_pos[61] = "R"
    new_board_pos[62] = "K"
    new_board_pos[63] = "1"
    new_fen_board = ChessFunctions.board_position_to_fen(new_board_pos)
    new_fen_string_full = new_fen_board + " " + "dummy_info"
    if ChessFunctions.is_king_in_check("w", new_fen_string_full) == True:
      return False

    # is square 61 or 62 under attack by black pawn?
    if board_pos[52] == "p" or board_pos[54] == "p": return False
    if board_pos[53] == "p" or board_pos[55] == "p": return False

    # is square 61 or 62 under attack by any black piece?
    for i in range(64):
      if board_pos[i] == "r" or board_pos[i] == "n" or board_pos[i] == "b" \
      or board_pos[i] == "q":
        piece_type_uncased = board_pos[i].upper()
        if ChessFunctions.can_reach(piece_type_uncased, i, 61, \
        board_pos) == True:
          return False
        if ChessFunctions.can_reach(piece_type_uncased, i, 62, \
        board_pos) == True:
          return False
      else:
        continue  # empty square or white piece/pawn

    # at this point, O-O is possible and legal
    return True

  # ----------------------------------------------------------------------------

  @staticmethod
  def can_white_castle_qside(fen_string_full):
    tokens = fen_string_full.split(' ')

    # check the FEN string
    if tokens[1] != "w": return False  # not white's turn
    castling_info = tokens[2]
    if castling_info.find("Q") == -1:
      return False

    fen_board = tokens[0]
    board_pos = ChessFunctions.fen_string_to_board_position(fen_board)

    # is position "R _ _ _ K" ?
    if board_pos[56] != "R": return False
    elif board_pos[57] != "1": return False
    elif board_pos[58] != "1": return False
    elif board_pos[59] != "1": return False
    elif board_pos[60] != "K": return False

    # is white king currently in check?
    if ChessFunctions.is_king_in_check("w", fen_string_full) == True:
      return False

    # attempt to castle into check?
    new_board_pos = ChessFunctions.fen_string_to_board_position(fen_board)
    new_board_pos[56] = "1"
    new_board_pos[57] = "1"
    new_board_pos[58] = "K"
    new_board_pos[59] = "R"
    new_board_pos[60] = "1"
    new_fen_board = ChessFunctions.board_position_to_fen(new_board_pos)
    new_fen_string_full = new_fen_board + " " + "dummy_info"
    if ChessFunctions.is_king_in_check("w", new_fen_string_full) == True:
      return False

    # is square 58 or 59 under attack by black pawn?
    if board_pos[49] == "p" or board_pos[51] == "p": return False
    if board_pos[50] == "p" or board_pos[52] == "p": return False

    # is square 58 or 59 under attack by any black piece?
    for i in range(64):
      if board_pos[i] == "r" or board_pos[i] == "n" or board_pos[i] == "b" \
      or board_pos[i] == "q":
        piece_type_uncased = board_pos[i].upper()
        if ChessFunctions.can_reach(piece_type_uncased, i, 58, \
        board_pos) == True:
          return False
        if ChessFunctions.can_reach(piece_type_uncased, i, 59, \
        board_pos) == True:
          return False
      else:
        continue  # empty square or white piece/pawn

    # at this point, O-O-O is possible and legal
    return True

  # ----------------------------------------------------------------------------

  @staticmethod
  def can_black_castle_kside(fen_string_full):
    tokens = fen_string_full.split(' ')

    # check the FEN string
    if tokens[1] != "b": return False  # not black's turn
    castling_info = tokens[2]
    if castling_info.find("k") == -1:
      return False

    fen_board = tokens[0]
    board_pos = ChessFunctions.fen_string_to_board_position(fen_board)

    # is position "k _ _ r" ?
    if board_pos[4] != "k": return False
    elif board_pos[5] != "1": return False
    elif board_pos[6] != "1": return False
    elif board_pos[7] != "r": return False

    # is black king currently in check?
    if ChessFunctions.is_king_in_check("b", fen_string_full) == True:
      return False

    # attempt to castle into check?
    new_board_pos = ChessFunctions.fen_string_to_board_position(fen_board)
    new_board_pos[4] = "1"
    new_board_pos[5] = "r"
    new_board_pos[6] = "k"
    new_board_pos[7] = "1"
    new_fen_board = ChessFunctions.board_position_to_fen(new_board_pos)
    new_fen_string_full = new_fen_board + " " + "dummy_info"
    if ChessFunctions.is_king_in_check("b", new_fen_string_full) == True:
      return False

    # is square 5 or 6 under attack by white pawn?
    if board_pos[12] == "P" or board_pos[14] == "P": return False
    if board_pos[13] == "P" or board_pos[15] == "P": return False

    # is square 5 or 6 under attack by any white piece?
    for i in range(64):
      if board_pos[i] == "R" or board_pos[i] == "N" or board_pos[i] == "B" or \
      board_pos[i] == "Q":
        piece_type_uncased = board_pos[i]
        if ChessFunctions.can_reach(piece_type_uncased, i, 5, \
        board_pos) == True:
          return False
        if ChessFunctions.can_reach(piece_type_uncased, i, 6, \
        board_pos) == True:
          return False
      else:
        continue  # empty square or black piece/pawn

    # at this point, O-O is possible and legal
    return True

  # ----------------------------------------------------------------------------

  @staticmethod
  def can_black_castle_qside(fen_string_full):
    tokens = fen_string_full.split(' ')

    # check the FEN string
    if tokens[1] != "b": return False  # not black's turn
    castling_info = tokens[2]
    if castling_info.find("q") == -1:
      return False

    fen_board = tokens[0]
    board_pos = ChessFunctions.fen_string_to_board_position(fen_board)

    # is position "r _ _ _ k" ?
    if board_pos[0] != "r": return False
    elif board_pos[1] != "1": return False
    elif board_pos[2] != "1": return False
    elif board_pos[3] != "1": return False
    elif board_pos[4] != "k": return False

    # is king currently in check?
    if ChessFunctions.is_king_in_check("b", fen_string_full) == True:
      return False

    # attempt to castle into check?
    new_board_pos = ChessFunctions.fen_string_to_board_position(fen_board)
    new_board_pos[0] = "1"
    new_board_pos[1] = "1"
    new_board_pos[2] = "k"
    new_board_pos[3] = "r"
    new_board_pos[4] = "1"
    new_fen_board = ChessFunctions.board_position_to_fen(new_board_pos)
    new_fen_string_full = new_fen_board + " " + "dummy_info"
    if ChessFunctions.is_king_in_check("b", new_fen_string_full) == True:
      return False

    # is square 2 or 3 under attack by white pawn?
    if board_pos[9] == "P" or board_pos[11] == "P": return False
    if board_pos[10] == "P" or board_pos[12] == "P": return False

    # is square 2 or 3 under attack by any white piece?
    for i in range(64):
      if board_pos[i] == "R" or board_pos[i] == "N" or board_pos[i] == "B" or \
      board_pos[i] == "Q":
        piece_type_uncased = board_pos[i]
        if ChessFunctions.can_reach(piece_type_uncased, i, 2, \
        board_pos) == True:
          return False
        if ChessFunctions.can_reach(piece_type_uncased, i, 3, \
        board_pos) == True:
          return False
      else:
        continue  # empty square or black piece/pawn

    # at this point, O-O-O is possible and legal
    return True

I tested my function by walking through several chess games, including Lasker-Thomas 1912 that featured an unusual king hunt and checkmate via castling queenside. I placed calls to the four can_castle functions inside the GameState.display_enhanced() function.

def main():
  print("\nBegin test can_castle() functions ")

  # 6. test via GameState.display_enhanced()
  fen_file = ".\\MiscFEN\\lasker_thomas_london_1912.fen"
  f = open(fen_file, "r")
  for line in f:
    print(line)
    gs = GameState.from_fen(line)
    gs.display_enhanced()  # calls four can_castle functions
    input()
  f.close()
  print("Done")
  input()

  print("\nEnd ")

A fun and interesting chess puzzle.



In “Sherlock Holmes Faces Death” (1943), Holmes (actor Basil Rathbone) unravels the mystery of the Musgrave Manor murders by solving a chess riddle.



The Mystery of the Missing Pawn

“The Mystery of the Missing Pawn” is a Sherlock Holmes pastiche (imitation) written by H. T. Dickinson and published in “The British Chess Bulletin” in January, 1911. I’ve condensed it and made a few changes.

It was a dreary November evening. Rain was falling outside, and to the best of my recollection there was a mist abroad, but on that point I am a trifle hazy. Sherlock Holmes was seated in his favourite chair with his violin under his chin playing snatches from the Merry Widow. I was seated on the floor, Holmes having the day before sawn one of the legs off my chair to use as a life preserver.

I jumped up intending to show Holmes the latest thing in jui jitsu, when the front door bell rang. “Someone for me, Watson,” said Holmes. He was right, for the next minute our landlady was ushering into the room a portly individual with a nose, the hue of which suggested liquids stronger than water.

“My name is Edwin Basker, of Basket Hall,” commenced our visitor, “and I am a Chess player. I always like to play with my own chessmen. I cannot explain why, but when I play with my own set of chessmen I win. Mr. Holmes, there is some underhand work going on. One of the pawns from my set of men has disappeared, and the championship match I play in commences tomorrow. Will you try to trace and recover the missing pawn for me?”

“Well, Mr. Basker, your case interests me, and I shall be happy to help you to recover your property. I think we had first better visit the scene of the crime.”

(a short time later at Basket Hall)

Without allowing Basket to finish his sentence, Holmes seized the teapot, and emptied the contents on the table. From amongst the tobacco cartridges he produced the missing pawn. Our client uttered a cry of joy, as his missing treasure came to light.

“It was a pretty little problem,” said Holmes. The simple explanation is this. You had arranged a match with another chess player, and consequently you were very excited about it, and when one of the pieces from your set happened to be missing, you jumped at the conclusion that your opponent had something to do with its disappearance. You were wrong. I examined the set of men, and adhering to the tops of some of the pawns, I found small particles of tobacco. You said you did not smoke yourself, but that your son did. I knew that the tobacco smoked was of a particular kind, which is only sold in cartridges, and is generally inserted in the pipe by means of a pipe filler. Now some people, I have observed, do not trouble to procure a pipe filler, but push the tobacco into the pipe with the finger or pencil. It struck me that the pawns from your set of men would make excellent fillers. You stated that your son smoked tobacco from cartridges, and further that he kept his unsmoked cartridges in the teapot. I examined the teapot, and found the missing pawn. Now, Mr. Basker,” concluded Holmes, “you have your cherished set complete.”

And after settling up certain little financial details too sordid to be dealt with here, Holmes and I made our way back to Baker Street.


Arthur Conan Doyle (1859-1930) wrote 56 Sherlock Holmes short stories and four short-ish novels. Most of the stories were published in “The Strand Magazine” (named after the London street where the magazine’s offices were located). The stories were illustrated by artist Sidney Paget (1860-1908). Paget illustrated 37 short stories and one novel. Paget’s art was a significant contributing factor to the immense popularity of the Holmes stories in the late 1800s and early 1900s.

Left: An illustration by Sidney Paget for “The Adventure of Silver Blaze” (December, 1892). Holmes solves the case of a missing race horse.

Right: A Paget illustration for “The Adventure of the Speckled Band” (February, 1892). Holmes solves the case of a murder who uses a poisonous snake. Considered one of the best Holmes stories by many people, including Conan Doyle.


This entry was posted in Programmatic Chess. Bookmark the permalink.

Leave a Reply