View on GitHub

PicChess

A playable chessboard on the PIC32 microcontroller
Brendan Sullivan (bzs5) and Elliot Hare (eph58)

Introduction

PicChess is a project to make a playable chessboard on the PIC32 microcontroller. Our goal for the project was to recognize and disallow any illegal moves, with all rules implemented, and to also offer a fully functional and adjustable game clock.

You can watch our demo video, and also read further to learn more about our project! Click the "View on GitHub" button at the top of the page to see our code.

High Level Design

Rationale and Sources

This project came out of what is being called the online chess boom of 2020. The boom is a result of several simultaneous events, including the release of Netflix's The Queen’s Gambit, as well as the unexpected popularity of PogChamps chess tournaments on Twitch.tv. It inspired both authors of the project to learn to play chess, and in the context of ECE4760, inspired us to pursue a chess game as the final project.

When starting to consider how to go about creating the software, we found a hackaday page which showed an impressive chess game running on a PIC32. This project was referenced for some ideas during our work, but mostly we knew the direction we wanted to go for our project ourselves, and did not take specific inspiration from the prior art or the code.

Background

The game is based on the authentic rules of chess, which are well-established. To avoid unnecessarily re-iterating them here, an official ruleset can be found here. The relevant sections are 1, 2, and 3 (everything after these are rules for in-person tournaments), and we implemented these sections fully and faithfully. Note that, in addition to the obvious rules governing how the pieces move and capture, we made sure to implement all the special rules that still govern the game. These include: castling, with full and accurate determination of when a player can castle; en-passant, which is a special type of pawn capture only allowable under certain conditions; promotion, which is when a pawn can become a queen, rook, bishop or knight upon reaching the opposite side of the board (In our game pawns will always promote to queens, since this is almost always the most desirable option. Most online websites offer players this “auto-queen” option for ease). We also take care that illegal moves regarding putting/leaving the king in check at the end of a turn are never allowed.

The serial interface is based on algebraic notation. From white’s perspective, the files (columns) are lettered a-h from left to right, and ranks (rows) from 1-8 upwards. Squares are at the intersection: a2, g4, etc. Generally in chess to specify a move, we give the letter of the piece and the destination square, e.g Qb2 (queen to b2). If multiple pieces can make the move, then to disambiguate we could say Qbb2, e.g the queen that is on the b-file moved to b2. If this is not enough, then we could say Qb4b2, for example. Usually algebraic notation is deliberately as short as it can be while being unambiguous, however there is a perfectly valid style called “Long Algebraic Notation” that will always include both initial and final squares, i.e Qb4b2. This is the style we use for the serial interface, since it avoids having to backtrack to find the piece and potentially make players re-enter moves for clarity.

The ending conditions for chess are wins and draws. Wins either occur via checkmate (specified in ruleset above) or resignation. There are several ways to draw: both players agreeing to a draw, stalemate (when a player cannot move but is not in check), insufficient material for either player to checkmate (for example, in a king and knight vs king endgame, the side with the knight cannot possibly checkmate and so the game is drawn), three-fold repetition (if an exact position occurs three times in a game, either player may claim a draw), and the 50 move rule (this rule has been changed several times and varies, but the essential idea is that 50 moves for each player without a pawn move or capture is considered a draw). All of these drawing methods need significant extra logic or even storage (particularly in three-fold repetition) to be implemented. Our philosophy is for our game to be between a physical gameboard and a fully online chess interface. As such, our game does not calculate any ending conditions, but it will never let players make illegal moves. If a player is stalemated or checkmated, they will not be able to move, and they will enter Resign or Draw into the serial interface.

One thing that we looked at when designing the game was Forsyth-Edwards Notation (FEN). This is a notation that specifies a current position in a game. In addition, it gives the current move, the castling right for each side (since once the king or relevant rook moves, the right is lost), on which square an en-passant can occur (if any), and relevant numbers for the 50-move rule. For example, the starting position is rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 This was inspirational to us because it told us exactly what state information we needed to track in addition to where the pieces are. In the interest of keeping our design simple, this is also the only state information we track, and we also cut out the 50 move rule stuff. We do also specifically track the king location, even though it’s slightly redundant, because not having to find it on the board speeds up the check logic drastically.

Logical Structure:

Main threads: playMove, timerUpdate, drawBoard, Serial
Main functions: isLegal, inCheck, legalCastle
The logical structure of our code is based on the PT threads we implemented. These threads flow from one another, such that when input is recognized over serial, the serial thread parses it and triggers the playMove thread. If the move is valid and legal, the board state is updated and the drawBoard thread is triggered. This thread updates the graphics on the TFT to show the current board state. A final thread, updateTimer is used along with the millisecond hardware timer maintained by Protothreads to keep a game clock running for both white and black. This thread yields for 500 milliseconds, then updates the timer values on the TFT. There are instances where the logic we coded is not exactly described here, but it will be covered in detail in the software section of this report.

Block diagram for the program:

Hardware-Software Tradeoffs:

Our program is mostly implemented using software. One tradeoff is that we deliberately use the protothreads timer rather than hardware interrupts for timing, as interrupts are overkill in this situation. Also, we use the TFT screen to avoid implementing graphics in software.

Standards and Acknowledgements

We believe our project adheres to all relevant IEEE standards, as it is an independently developed student project. The game of chess is public domain. We do have one acknowledgement: To create the sprite graphics for the pieces, we chose to find a sprite set that we could import to our code. The sprites we found were drawn by Ajay Karat of Devil’s Workshop. Karat has chosen to release the sprites to the public domain under CCO1.0 Universal, waving any copyright to the designs. We are thankful to Karat for providing their work.

Implementation Details

Program Details

To program the game, we focused first on the graphics before the logic. The TFT_gfx header file includes many functions to assist in drawing to the screen. For the board, we drew alternating light and dark squares, then a border around the board. To draw the pieces, we used the drawbitmap function. This required converting our sprite set to bitmaps, then using matlab to extract and format the bitmap data as arrays that could be written as a header file to the project. The graphics are updated when a move is played.

The serial thread is what we used to build the player interface. The GUI itself is written in python, and mainly just has a string input for the player. Before the first move is played, players can update the time control to either bullet (1 min), blitz (5 min), rapid (10 min), and classical (30 min). The serial thread parses these inputs and updates the max time for both players. Players can also type “inc x” to set the increment on each move to be x seconds (including 0). This is also handled within the serial thread.

Otherwise, if a move is passed into serial, the playmove thread will attempt to parse it. Firstly, if the move is a draw or resignation, the game state is updated (0 = normal game, 1/2/3 are white/black/draw). The new_move flag is set. If the move is a standard move, we can parse the piece (including color), the original square, and its destination. Along with whose turn it is, we pass this information into the isLegal function. Before doing this, we check that there is actually the piece to be moved at the initial square. When in the function, we first use the onBoard helper function to make sure the destination square exists.

From here, this function works differently for each piece. For pawns, it first checks if the move is on the same file (a push), and then that this move either only goes one square forward or two if on the starting space, and that the move is unblocked by another piece. If the move is not on the same file, it must be a capture, and we check that it is capturing on a legal square and that there is an enemy piece to capture (including via en-passant). Knights are easier because they cannot be blocked, and along with all other non-pawns they capture by moving regularly, so we just check that the move is in the characteristic L-shape and is not onto a piece of the same color. Rooks and bishops are both “sliders,” so we check that a) they move in the proper direction, b) that they are not blocked on their way to the destination square, and c) that the destination square is either empty or a capturable piece. The queen is done by recursively checking if either a rook or bishop could make the move, as the queen combines both. The king is then done by checking the move is a legal queen move that only moves one square. As such we avoid code reuse.

The next thing to do is check that the move does not leave the king in check. To do this, we call the inCheck function, and provide the parameters who just moved and the location of their king. Our philosophy for this function is to work backwards from the king, rather than looking at each piece and if it is attacking the king. We check if there are any knights putting the king in check (8 possible squares), then iterate over each diagonal to see if any pawns, queens, and bishops, or even opposing kings are attacking the king, then do the same on straight lines for rooks, queens and kings. For these checks, we end when we either run off the board or see a piece of the same side as the king (blocking check).

In the case that we are in check, the move is illegal. One special scenario is that, if we move the king, we need to see if it is in check using its new location, but if it is, we must restore the old location. For a king move, we track the old location just for this case. If the move passes the isLegal and inCheck tests, it is then legal, and the piece is moved (update the board). We must potentially update the king location, castling rights, and en passant (although some or all of these may not change). Finally, we update the turn, and set the new_move flag.

The other scenario for the serial thread is when the player enters O-O or O-O-O, corresponding to kingside and queenside castling respectively. To check the legality of castling, we call the legal_castle function and pass in the color and side. The function first checks if the color/side castling rights are intact, then checks if the castle is blocked by any pieces, then checks if any of the squares the king starts on, goes to, or travels over (3 squares in all cases) are in check. This is because the king cannot castle out of or into check and in fact is not allowed to castle over an attacked square. If these checks pass, then we perform the castle (moving both pieces) and update castling rights, king location and the turn before setting new_move.

Thus, when playMove exits successfully (i.e the serial input is valid), the new_move flag will be 1 (set). For a normal game (state = 0), The drawBoard thread calls the drawBoard helper function, which draws the board as mentioned above. If the game has ended, a screen is shown with the outcome of the game.

The timer thread, timerUpdate, uses the PT_GET_TIME() function built into protothreads. It will run every 500ms and decrement the timer depending on whose turn it is, and update this on the screen so the players can see. If a player times out, this thread updates so we can see that the game has ended as a result of this, and we see a win screen.

Hardware Details

Hardware is the PIC32 and TFT screen described on the course website. A schematic can be found in the appendix. We did not use any additional hardware.

Suggestions

Mostly, everything we tried worked, with one exception. We found that, although using hardware interrupts worked, it was better to use the protothreads time for the clock. In terms of alternate options, there are a few things to mention. Firstly, for seeing if the king is in check, it is possible to iterate over the enemy pieces rather than iterating backwards from the king. This option is probably preferable in a flexible language like python, but in C maintaining a list of the enemy pieces additionally to the board is more annoying. Another thing is that we had the board as a simple array of ints, although sometimes people use bitboards to lower the memory needed.

Results

Images

Starting screen for a 10 minute (rapid) game

Python GUI for entering moves

Other Comments

Speed is not hugely problematic since chess is a slow moving game. The most time intensive operation in our project is redrawing the screen. To quantify the draw speed, footage of playing the game was scrubbed through. It took 10 frames of the recording from the time a move was sent to the time the screen was fully redrawn with the new position. The video was playing back at 25 frames per second, so it takes roughly .4 seconds to redraw the screen. This speed is sufficient such that the players will be slower than the code to make moves.

Interacting with the game happens over a text based serial terminal. This is perfectly functional, as describing moves with algebraic notation is commonplace among chess players, however this is not the most streamlined nor the most disability-accessible input method. A graphic input method, or as mentioned in our proposal a voice recognition and TTS input method could expand the accessibility, and would be a future goal for further exploration.

Conclusions

Reflections

Comparing the delivered project to the proposal we wrote, we were very successful. We wanted to have a playable game between two players over serial, and we achieved that goal. Also in the proposal we discussed an engine, or bot, to play against. This was not implemented, but it was always intended as a stretch goal.

Were we to start again, I think our implementation of the rules of the game would not change very much, as it is the most direct interpretation of chess rules in code we could create. Ideally we would have spent time to add engine functionality, but that is additional to the functionality we included so it would not change the programming otherwise. We would make slight changes to the feedback given over the serial interface, as there were some aspects that were vague or not functioning exactly as intended. Specifically, when changing the time settings and increment, the serial terminal only informs players of the most updated settings when they play the first move, instead of when the time format change is made. Also, when attempting illegal moves, sometimes specific reasons are given why a move is disallowed, but other times just ‘invalid move’ is printed. More clarity in why a move is invalid may be helpful to the players.

Ethical Considerations

We believe that our project faithfully upholds the IEEE Code of Ethics. We also believe there is little capacity for our project to do harm or endanger the safety of its users. That being said a multiplayer game, such as chess, has the capacity for players to act unjustly or rudely to one another. This project does nothing to facilitate these behaviors, but they may be out of our control.

Final Acknowledgements

This project, and more broadly ECE4760 as a whole, was interesting, fun, and educational. We would like to thank the course staff, Hunter Adams, Bruce Land, and the ECE department for making this class such a good experience.

Appendicies

Appendix A: Release

The group approves this report for inclusion on the course website.

The group approves the video for inclusion on the course youtube channel.

Appendix B: Code

Code can be found in the corresponding github directory linked from this page

Appendix C: Schematics

Schematic:

Appendix D: Budget Considerations

We did not buy any additional hardware for this project.

Appendix E: Task Breakdown

Elliot designed the initial board graphics and did most of the work on the final timer implementation. Brendan converted the bitmaps and wrote the legalCastle function. The rest of the logic was done via partner coding over zoom.

Appendix F: Credit

Sprites: Ajay Karat | Devil's Work.shop http://devilswork.shop/