While watching The Queen’s Gambit, I was reminded about how much I used to enjoy playing chess. I was eager to play a game, so I started to tweet, “D2-D4” knowing that some of Twitter followers would recognize this as an opening move and likely respond with their move, giving me the fix I needed. I paused before hitting the tweet button because I realized that I’d need to set up a board (physical or virtual) to keep track of the game. If I received multiple responses, I’d need multiple boards. I decided not to send the tweet.
Later in the day, I had the idea to create a simple service that enables my use case. Instead of designing yet another chess site (I built one with a friend in 2009 that is long since gone), I decided to create a chess board logger and visualizer to make it practical to play via Twitter or any other messaging/social platform. I didn't have a lot of time, so I was optimizing for simplicity.
Instead of tweeting moves back and forth, players tweet links back and forth, and those links go to a site that renders the current chessboard, allows a new move, and creates a new link to paste back to the opponent. I wanted this to be 100% serverless, meaning that it will scale to zero and have zero maintenance requirements. Excited about this idea, I put together a shopping list:
My MVP requirements:
Represent the board position—ideally completely in the URL to keep it stateless from a server-side perspective
Display a chessboard and let the player make their next move.
Stretch goals:
Enforce chess rules (allow only legal moves).
Dynamically create a png/jpg of the chessboard that I can use as an Open Graph and Twitter card image so that when a player sends the link, the image of the board will automatically display.
Putting it all together
Representing the board position
There is a standard notation for describing a particular board position of a chess game called Forsyth–Edwards Notation (FEN) that was exactly what I needed. A FEN is a sequence of ASCII characters. For example, the starting position for any chess game can be represented by the following string:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Each letter is a piece: pawn = "P", knight = "N", bishop = "B", rook = "R", queen = "Q" and king = "K". Uppercase letters represent white pieces and lowercase letters represent black. The last part of the string is specific to certain rules in chess (read more about FEN).
I knew I could use this in the URL, so my first requirement was complete and I was able to represent the board state in the URL eliminating the need for a backend data store.
Displaying the chessboard and allowing drag-and-drop moves
Numerous chess libraries are available. One in particular that caught my eye was chessboard.js—described as “a JavaScript chessboard component with a flexible ‘just a board’ API”. I quickly discovered that this library can display chess boards from a FEN, allow pieces to be moved, and update the FEN. Perfect!
In only two hours, I had the basic functionality implemented.
Enforcing chess rules
I originally thought that making this service aware of chess rules would be difficult, but then I saw the example in the chessboard.js docs showing how to integrate it with another library called chess.js—“a JavaScript chess library that is used for chess move generation/validation, piece placement/movement, and check/checkmate/stalemate detection—basically everything but the AI”. A short time later, I had it working! Stretch goal #1 completed.
Here's what a couple of game moves look like:
Moving the pawn from D2 to D4 in a new game—https://chessmsgs.com/?fen=rnbqkbnr%2Fpppppppp%2F8%2F8%2F3P4%2F8%2FPPP1PPPP%2FRNBQKBNR+b+KQkq+d3+0+1&to=d4&from=d2&gid=mOhlhRlMboYsHLqBF1f7I
Black countering with a similar move of pawn from D7 to D5—https://chessmsgs.com/?fen=rnbqkbnr%2Fppp1pppp%2F8%2F3p4%2F3P4%2F8%2FPPP1PPPP%2FRNBQKBNR+w+KQkq+d6+0+2&to=d5&from=d7&gid=mOhlhRlMboYsHLqBF1f7I
The URL has the following data:
fen—the new board position
from and to—indicating what move occurred (I use this to highlight the squares)
gid—a unique game ID (I used nanoid)—I’ll use this to connect moves to a single game in the future. For example, I could add a feature that lets the user request the entire game transcript).
Done! Except...
At this point, there were no server requirements other than simple HTML static hosting. But after playing it with some friends and family, I decided that I really wanted to accomplish the other stretch goal—dynamically create a png/jpg of the chessboard that I can use as an Open Graph and Twitter card image. With this capability, an image of the board will automatically display when a player sends the link. Without it, the game is a series of ugly URLs.
Dynamically creating the Open Graph image
This requirement introduced some server-side requirements. I needed two things to happen on the server.
First, I needed to dynamically generate a board image from a FEN. Once again, open source to the rescue (almost). I found chess-image-generator, a JavaScript library that creates a png from a FEN. I wrapped this in a bit of Node.js/Express code so that I could access the image as if it were static. For example, here’s a demo of the real endpoint: https://chessmsgs.com/fenimg/v1/rnbqkb1r/ppp1pppp/5n2/3p4/3P4/2N5/PPP1PPPP/R1BQKBNR w KQkq - 2 3.png. This link results in this image:
Second, I needed to dynamically inject this FEN-embedded URL into the content attribute of the meta tag in the main HTML. Like me, you might be thinking that you could just do some DOM manipulation in JavaScript and avoid having to dynamically change HTML on the server. But, the Open Graph image is retrieved by a bot from whatever service you use for messaging. These bots don’t execute any client-side JavaScript and expect all values to be static. So, that led to additional server-side work.
I needed to dynamically convert this:
<meta property="og:url" content="{{url}}" />
Into something like this:
<meta property="og:url" content="https://chessmsgs.com/?fen=rnbqkb1r/ppp1pppp/5n2/3p4/3P4/2N5/PPP1PPPP/R1BQKBNR+w+KQkq+-+2+3&to=f6&from=g8&gid=ziL3VfMEoIT9iNwp6csBh" />
I could have used one of many Node templating engines to do this, but they all seemed like overkill for this simple substitution requirement, so I just wrote a few lines of code for some string.replace() calls in my Node server.
With this functionality added, a game on Twitter (and other services) now looks much better:
Check out the code
The source for chessmsgs.com is available on GitHub at https://github.com/gregsramblings/chessmsgs.
Deciding where to host it
The hosting requirements are simple. I needed support for Node.js/Express, domain mapping, and SSL. I was working at Google Cloud at the time and wanted to go completely serverless, which quickly led me to Cloud Run. Cloud Run is a managed platform that enables you to run stateless containers that are invocable via web requests or Pub/Sub events.
What’s next?
If I really wanted to engineer this for extreme loads, I could easily deploy it to multiple regions throughout the globe and set up a load balancer and possibly a CDN.
When I first started thinking about the image generation, I naturally thought about caching many common images in Google Cloud Storage. This would be easy to do and storage is crazy cheap. But, then I did a bit of research and learned the following fun facts. After two moves (one move for each player), there are 400 different distinct board positions. After each player moves again (two moves each), this number is now 71,782 distinct positions. After each player moves again (three moves each), the number is now 9,132,484 distinct positions! I could gain a bit of performance by caching the most popular openings, but each game would quickly go beyond the cached images so it didn’t seem worth it. By the way, to cache every possible board position would be about 1046 positions, which is a massive number that doesn’t even have a name.
Conclusion
This was a fun project – almost therapeutic for me since my “day job” doesn’t allow much time for writing code.
Now that I work at AWS (managing technical content, documentation, SDKs, CLI), I might use this as an excuse to play with Lambda. I just need more time!