Software

Mock System Diagram
Hardware Interface Software
Our plan is to implement a software system using two processing units, a Raspberry Pi and an ESP32 microcontroller.
The Raspberry Pi will run a game loop to keep track of game state (whose turn it is, past moves, etc.), compute moves using our
custom AI, and process images taken of the board to compute board state after the player moves a piece. The AI move is then
converted to a standardized message format (see
OpCode) and sent to the ESP32 microcontroller using UART
serial communication. The microcontroller then actuates the physical board mechanism (motors and electromagnet) and sends back
status messages indicating current state of the board/microcontroller (see
ArduinoStatus). UART serial
communication is used as it allows us to easily send data in both directions.
Piece recentering capability
The centerPiece() function is used after a player makes a move to ensure that the player's chess piece placement is
aligned properly and does not interfere with the board's piece movement. The function is composed of concentric circle approximations
and shifts between the circles, with calls to makeCircle() and moveToNextCircle() respectively.
The makeCircle() function receives
two parameters, int circle and int firstQuarter. The function moves the electromagnet along the given circle size, starting from the
given quarter (where 0=top, 1=left, 2=bottom, 3=right). The movements are restricted to slopes that can be generated through combinations
of the EasyDriver MS1 and MS2. Each direction can be set to 1/8, 1/4, 1/2, or full step size. This allows for slope settings of 0, 1/8, 1/4, 1/2, 1, 2,
4, 8, and vertical. Moving the x and y motors in paired steps prevents the magnet from making a stair-step style movement, and increases
movement speed by reducing the amount of logic required to make the movements. During the movement, a previously computed number of moves
is made at each slope. Several of the slopes have two different scales in order to further accelerate the movement. A slope of 1, for example
, moves as far as possible with full steps in the x and y directions, and switches to ⅛ scale steps in both directions for the last 1-7
movements.
The number of steps at each slope is computed by the calculatePulsesPerSlope()
function, which is only called once in the setup() initialization function. It calculates the instantaneous slope of a circle at the
current point, approximates it to one of the available slopes, then increments the x and y position for that slope. It repeats this
process until the first quarter of the circle is complete. The remaining three quarters of the circle use the same pulses per slope
counts with different x and y directions and/or reversed order.

Figure 1: Example of circular motion composed of multiple tangent line
approximations
An example of the slope calculation process is shown (Figure 2). The initial point is at the topmost
part of the ideal circle. Movement begins with horizontal steps to the left. After each step, the slope is recalculated using a tangent
line to the concentric circle that passes through the point. The slope of the tangent line is then approximated to one of the nine slopes
that can be generated by the EasyDrivers, and is used for the next step. Note that the difference between the path and goal circle is greatly
exaggerated for visualization purposes.

Figure 2: Visualization of tangent line computation to obtain instantaneous
slope
The getInstantaneousSlope() function calculates the instantaneous slope, dy/dx, of a circle at the provided point. The
calculation is derived from the parametric equations for a circle.The current x and y coordinates are calculated using the number of steps remaining
in each direction and the radius of the circle.
The moveToNextCircle() function moves the electromagnet from the current circle to a smaller
concentric circle in two motions (see Figure 3). The first motion has a slope of ±1 and aligns the magnet with the side of the
next circle. The second motion is a vertical or horizontal movement to reach the starting point for the next circle. This is much
easier to calculate than a direct movement, given our slope constraints. Additionally, it allows the magnet to cover more area
than a direct movement. The final call to moveToNextCircle() implicitly resets the electromagnet to the center of the square.

Figure 3: Path from outer to inner concentric circle composed of diagonal +
straight line
Game State Managment
The game state management code is comprised of a few fundamental components:
- The entry point which is the main Game loop
- A BoardInterface that wraps communication with the chess board as well as a chess engine for maintaining board state
- Various Player models to support different modes of interaction
- ArduinoStatus and OpCode enums that define message passing between Pi and Arduino
Game
After setting up the connection to the board and doing startup tasks (such as determining who moves first), the
general structure of the game loop is as follows
- Get the board status from the arduino (see ArduinoStatus)
- If the status is EXECUTING_MOVE or MESSAGE_IN_PROGRESS, continue
- If the status is ERROR, handle the error
- If it is the human's turn
- Wait for the END_TURN_BUTTON_PRESSED signal from the Arduino
- If it is the AI's turn
- Get move from the neural network
- If move is valid, send it to the board, else, get the next best move
- While not checkmate/stalemate, repeat
We plan to set up an architecture for a queue of moves: Each loop, we try to dispatch one move from the queue.
If the queue is non-empty, we dispatch moves until it is. In order to pop the front of the queue, the Arduino needs to send
back the move count of the previously dispatched move and idle status. Only then do we pop that move from the queue and start
sending the next move to be processed. Once the queue is empty, we proceed to the rest of the loop.
BoardInterface

Diagram showing indexing scheme, graveyard, capture squares, and promotion area
Board
The Board class is the interface point between the Arduino and the Raspberry Pi. Board is used to send and
receive messages through the Board interface (see
ArduinoStatus and
OpCode), validate
moves, and handle captures/promotions.
Engine
We construct a simple wrapper around
python-chess and add some helper functions to keep track of board state.
We formulate the Graveyard to avoid the need to use computer vision to keep track of captures and promotions.
By having a capture square, we can determine from the state of the board which piece was captured, and automatically move it to
a specified position in the graveyard. When the user wants to promote their pawn to a new piece, they select the replacement
piece from the designated promotion area. When the board state determines a promotion happens, the promotion area is backfilled
from the other pieces in the graveyard (as long as there are spare pieces available).
Player
We eventually plan to allow users to play the board through
- Physical interface: User moves physical pieces on the chessboard
- Command line interface (CLI): User specifies pieces to move in grid algebraic notation (GAN) format (e.g. e2e4)
- Web graphical user interface (GUI): User specifies pieces to move by dragging and dropping (similar to Lichess or chess.com)
- Voice interface: User specifies pieces to move using voice commands (see Speech Recognition)
This is an enum of operation codes used to provide information about the type of move the Arduino should make.
- MOVE_PIECE_IN_STRAIGHT_LINE: A code to indicate that the Arduino should use a straight-line path (diagonals allowed)
- MOVE_PIECE_ALONG_SQUARE_EDGES: A code to indicate that the Arduino should move the piece along square edges instead of through
the center of squares. This is used for knights if adjacent pieces are in the way, and for moving pieces to and from the Graveyard,
and for castling.
- ALIGN_PIECE_ON_SQUARE: A code to indicate that the Arduino should use the electromagnet to center piece on square (used after
human moves).
Note that queenside and kingside castling are composed of two moves; one move of type 1 (king moving in straight line)
and one move of type 2 (rook moving around king).
Messages sent from Pi to Arduino have the following format:
"~<OpCode><StartPosition><EndPosition><MoveCount>"
~ is a delimiter; OpCode is as described above; StartPosition and EndPosition are 2-character strings of the form <A-L><A-L>
to index the 12x12 array (they are converted on the Arduino to two integers in the range of [0, 11]; MoveCount is an integer that serves as
a handshake between the Arduino and Pi. Move number <MoveCount> is sent until the Arduino responds with the same <MoveCount> and
an IDLE status (see
ArduinoStatus), after which move number <MoveCount + 1> is sent if it is available. Note that we
send `MoveCount % 255` since a byte only allows sending 256 0-indexed values. ExtraByte is used whenever there is an error and is used to pass
along additional information about the type of error.
This is an enum of status codes used to provide information about the current operational state of the Arduino.
- IDLE: A status to indicate that the Arduino is waiting for the next move.
- MESSAGE_IN_PROGRESS: A status to indicate that the Arduino is currently processing a message from the Pi.
- EXECUTING_MOVE: A status to indicate that the Arduino is currently executing a move.
- END_TURN_BUTTON_PRESSED: A status to indicate that the human player pressed the end turn button. Pi should then take a picture and
compute the difference in board state.
- ERROR: A status to indicate that an error occurred.
Messages sent from Arduino to Pi have the following format:
"~<ArduinoStatus><MoveCount><ExtraByte>"
~ is a delimiter; ArduinoStatus is as described above; MoveCount is as described in
OpCode. From the Arduino side,
MoveCount serves as a handshake between the Arduino and Pi to confirm that move number <MoveCount> is either being executed (EXECUTING_MOVE),
was the last move executed (IDLE), or is the move currently being sent (MESSAGE_IN_PROGRESS). When the Pi receives an IDLE status from the Arduino
along with move number <MoveCount>, the Pi sends the move corresponding to <MoveCount + 1> if it is available. ExtraByte is used whenever
there is an error and is used to pass along additional information about the type of error.
Chess AI
For the AI and computer vision side of the project, our plan is to utilize a variety of techniques from AI and combine them into
the most efficient algorithm, parallel to the algorithm implemented by AlphaZero.
The chess AI will be trained using the Actor Critic algorithm
for reinforcement learning, parameterized by a neural network (a network of equations that takes in input(s) and returns output(s)). We may initialize
the network parameterization using supervised learning over a training set of existing chess games played by humans to allow learning to get off to a
faster and more efficient start. We will decide this by comparing its efficiency to an AI that learns exclusively through self play.
To test the
efficiency and utility of different machine learning approaches to chess AI, we will compare the convergence rate and style of play of our AI to other
state-of-the-art methods. We will then find which approach is the most suited for learning in as little time as possible and compare which approaches
lead to contrasting playing styles. This will include testing against other chess AIs such as Stockfish, Maia, and Leela Chess Zero, and also human players.
When testing against people, we plan to ask them about their experience playing against our AI and to rate how it plays.
Our plan involves utilizing computer vision to both detect where pieces are on the board initially and where the pieces get moved by the player.
Our system will determine the location of each piece based on the difference in the board state after each move. Since only a fixed number of pieces can be
moved at once, we can cross reference the board state changes with the rules of chess to keep track of the locations of each piece throughout the game.
This will be used to determine where the AI is allowed to move a specific piece on the board, if a human player makes an error, or where to find a new piece
if a pawn promotion is necessary. In summary, our problem statement is to use the state of the board at time t-1 and an image of the board at time t to get
the board state at time t.
Preliminary Ideas

Outline of ideas about computer vision
Approach
Our approach given the above problem statement is as follows:
- Take a picture of the board from a fixed angle (top down or skew? TBD)
- Perform edge detection with OpenCV (possibly using Canny edge detection)
- Perform line detection with OpenCV (possibly using Hough edge detection)
- Compute intersection points with OpenCV (possibly using findContours)
- Find corners of the chess board
- Compute transformation matrix to constant image dimension (e.g. 800x800)
- Transform/crop/resize image based on transformation matrix and computed corners
- Segment board into 64 squares
- For each square
- Check if square is occupied
- If occupied, determine piece color
- Determine difference between B[t-1] and B[t] to compute delta_matrix[i][j]
Using pixel difference to compute change in board state. If a square is either empty in both or occupied in both images, we
assign a 0 to the corresponding cell (to indicate no difference in board state for that cell); if a square is occupied at time t and is
empty at t-1, we assign a -1; lastly, if a square was empty at time t and is occupied at t-1, we assign a 1.
- If needed, handle piece promotion
- Approach 1: Use a neural network based classifier to determine which piece was chosen for promotion
- Approach 2: Ask user to specify (via GUI or CLI) which piece they chose for promotion
Steps 2-6 are done a single time at program startup; we can then reuse the computed values to segment the board and
isolate each square.
To avoid using computer vision to keep track of the Graveyard state (see
Graveyard), we
have a capture square and designated promotion spaces in the graveyard so that we only need computer vision for the main part of the board.
The
Harry Potter scene
where Harry and Ron give commands to move pieces using their voice alone while playing Wizard's Chess inspired us to implement something similar
for this project. We use a
python wrapper
of Google's speech recognition API to convert audio input to a representation that is compatible with our chess engine.
Our web application will consist of a front end web app built using Javascript that uses the
chess.js and
chessboard.js libraries. To communicate with the
chessboard, we need two way communication. One option we are considering is to use a flask backend that updates a database when moves are made;
this can then be polled by the frontend and game loop to get the most recent move. Another option is to use web sockets to allow direct two way
communication without the need to poll a database. In either case, we need to set up authentication of some sort to ensure that only authorized
users are sending moves to the chess board, but we want to publish the current state of the board to a non-auth-protected page for anyone to
follow along with the current game. We also want to make user profiles to track statistics like win-loss record, games played, and elo, among
other attributes. Lastly, we aim to set up a live-stream of the physical chess board and host that alongside the other components of the web app.