NTL

Automating chess with my voice

Chess boards are an 8x8 grid where rows and columns are represented by the numbers 1-8 and the letters a-h and therefore each square can be represented by a combination of both, for example b5 or h3.

I decided that Python would be a good choice for this project because of how quickly and easily I could implement and test everything.

I used the following libraries,

I needed a way to know where each chess piece was displayed on the screen and which color I am playing as. I made a calibration function to initialize everything,

import pyautogui as p
from PIL import ImageGrab
def calibrate():
  global result, res, square_size
  x_positions = []
  y_positions = []

  try:
    board_pos = p.locateOnScreen("white.png", confidence = 0.85)
  except (p.ImageNotFoundException, AttributeError):
    try:
      board_pos = p.locateOnScreen("black.png", confidence = 0.85)
    except (p.ImageNotFoundException, AttributeError):
      print("Board not found")

  left = board_pos.left
  top = board_pos.top
  right = left + board_pos.width
  bottom = top + board_pos.height
  square_size = board_pos.width / 8
  print(square_size)
  left -= square_size / 2
  top -= square_size / 2
  color = "black"
  for i in range(8):
      x_positions.append(left + square_size*(i + 1))

  for i in range(8):
      y_positions.append(top + square_size*(i + 1))

  for i in range(7, -1, -1):
      for j in range(7, -1, -1):
          square = letters[i] + str(numbers[j])
          positions[square] = [x_positions[len(x_positions) - i - 1], y_positions[j]]

  im = ImageGrab.grab().load()
  for squ, coor in positions.items():
    squares[squ] = im[positions[squ][0] + 55, positions[squ][1] + 55]

  for key,value in squares.items():
    if result.count(value) <= 2:
      result.append(value)
      res.append(key)

  first_color = im[positions["e4"][0], positions["e4"][1]]
  second_color = im[positions["d4"][0], positions["d4"][1]]

  return first_color, second_color

After creating a new board and assigning a state, I had to figure out how to programmatically control the board. To do this, I need to know where the board is on the screen, which color I am playing as, and what moves my opponent makes.

The next step was answering the question of how to get words from sound. I debated using the speech recognition web API but it was not compatible with the Firefox browser I was using. Instead, I opted to use Google's API, as it was very easy to use and only took about 10 lines of code. I defined a method to get speech to text that records from the microphone and then makes a request for either a string, if one was recognized, or a printed error. It looks like this,

import speech_recognition as sr
def record():
  r = sr.Recognizer()
  with sr.Microphone() as source:
    print("Recording")
    audio = r.listen(source)
    try:
      text = r.recognize_google(audio);
    except sr.UnknownValueError:
        print("Audio could not be understood")
    except sr.RequestError:
        print("Request could not be completed")
    return text

I then needed to parse the move from the recognized text.

def parse_move(text):
  san = "" 
  pieces = {"K": ["king"],
          "Q": ["queen"],
          "R": ["rook"],
          "B": ["bishop"],
          "N": ["knight", "night"],
          "0-0": ["castle", "castles"],
          "0-0-0": ["long"],
          "": ["pawn"]
          }

  move = text.lower().split()
  print("full-text: " + text)

  piece = move[0]
  square = move[-1]
  fyle = square[0]
  try:
    rank1 = square[1]
  except IndexError:
    print("IndexError")
    return "error"
  try:
    rank = int(rank1)
  except (ValueError, UnboundLocalError):
    print("Issue getting move")
    return "error"

  for key, value in pieces.items():
    for p in value:
      if piece == p:
        san += key

  if san == "0-0":
    return board.parse_san(san)
  if san == "0-0-0":
    return board.parse_san(san)

  if fyle not in letters:
    print("Coordinate not recognized")
    return "error"

  try:
    if rank < 0 and rank > 8:
      print("Rank not recognized")
  except UnboundLocalError:
    print("Error, no rank")
    return "error"

  print("file: " + fyle)
  try:
    print("rank: " + str(rank))
  except UnboundLocalError:
    print("Error, no rank")
    return "error"

  try:
    san += fyle + str(rank)
  except UnboundLocalError: 
    print("UnboundLocalError")
    return "error"
  print(san)

  try:
    san = board.parse_san(san)
  except ValueError:
    print("ValueError")
    return "error"
  return san

This function validates and makes the move using the python chess library.

def chess_make_move(board, move):
  from_square = move.uci()[:2]
  to_square = move.uci()[2:]
  if is_legal(board, move.uci()):
    make_move(from_square, to_square)
    board.push(move)
  else:
    if board.is_game_over():
      game_over = True
    else:
      print("Move not able to be played")

This function will use pyautogui to control the mouse and make the correct move on the board based on it's location on the screen.

def make_move(origin, destination):
  p.moveTo(positions[origin][0], positions[origin][1])
  p.click()
  p.dragTo(positions[destination][0],
           positions[destination][1],
           0.35, button='left')

handle_moves() will take an image of the screen, search through each square on the board, and find the move that was made by the opponent. After figuring out the move, it will be added to the 'board' object to maintain the game state.

def handle_moves():
  global res, result, move_number
  p.hotkey("command", "tab")
  im = ImageGrab.grab().load()
  p.hotkey("command", "tab")
  for squ, coor in positions.items():
    squares[squ] = im[positions[squ][0] + 50, positions[squ][1] + 50]

  for key,value in squares.items():
    if result.count(value) <= 2:
      result.append(value)
      res.append(key)
  print(res)
  li1 = res[-1]
  li2 = res[-2]
  if im[positions[li1][0], positions[li1][1]] == squares[li1]:
    move = li1 + li2
    print("move - " + move)
    try:
      board.push_uci(move)
      move_number += 1
    except ValueError:
      print("Move - " + move + " invalid")
  if im[positions[li2][0], positions[li2][1]] == squares[li2]:
    move = li2 + li1
    print("move - " + move)
    try:
      board.push_uci(move)
      move_number += 0
    except ValueError:
      print("Move - " + move + " invalid")

The main game loop begins by initializing and calibrating the board state, then will loop until game over and prompt the user to speak the moves when it is their move.