How I Designed + Built A Poker Site
By: Jay Brower
Tags: Poker, Backend, Software, Architecture

The “JackNine” NLH Table

Check Out The Code Here

Inspiration

It’s roughly 10pm in April of 2020. A novel coronavirus had recently incentivized staying indoors. I was on discord with roughly a dozen of my close friends, wrapping up a fairly heated game of CS:GO.

“Let’s play poker!”, someone offered.

I googled for some poker sites that would allow us to play a non-cash game of NLH, with controlled buy-ins, lightly-customizable rules (i.e straddles), and for free. I found astonishingly little. This was stunning — I expected a sea of terrible web-apps offering poker implementations.

I thought about writing a discord bot to host a poker game. The bot might send private messages with your cards, and update a publicly viewable comment containing the state of the board. Players could take actions by commenting in a thread, after which the bot would process the action and progress the game. While this would work, it didn’t seem fun to build. And I was very bored.

After a frantic 30 minutes, I decided to build a poker site. I wanted to see what modern web apps looked like — and this was a good testbed to experiment with that. I scribbled out a napkin-diagram of an architecture for the game, and set out coding.

If you just want to see code, check out the Github.

“napkin” diagrams + straw-man architecture

The architecture behind many major web applications can be roughly approximated with what I call a “straw-man architecture”. I’ll produce a list of constraints and features, and after some meditation write down a sample solution. It doesn’t need to be perfect — it can have glaring issues with scalability, performance, or extensibility. Writing out this architecture will cause you to ask: “why can I recognize that this is glaringly wrong?”.

I call these napkin diagrams, since they should roughly fit on a small piece of paper, and you’re very likely to throw them out after creating them.

features = getFeaturesAndRequirements(); strawman = brainstormArchitecture(features); while (confusedBy(strawman)) { strawman = brainstormArchitecture(features); }

Worrying about scalability, extensibility, or any other maturity level of your codebase might keep you from adding serious value with your software early on. These are all good problems to have.

A Poker Straw-man

>> diagram 1- a very simple poker architecture

The architecture is designed to be as simple as possible. Each server host is a node.js deployed webserver. Clients establish a Websocket connection with the server, which they then issue commands to when playing the game. The server can routinely broadcast information including game state, server state (“going down for maintenance”), and other goodies over this channel.

Data is stored in redis (a fast in-memory cache) for convenience- the active game state for card games is pretty small, and we can host 1000’s of concurrent games per host with this. We then sync game-state to a mongodb cloud instance running remotely, which can be used to seed new host machines with a game. Redis, Mongo, and Heroku all offer “free” tiers of service for developers, so you shouldn’t need to pay until your service scales to users.

A note on Architecture

I chose Typescript + React for my client, so that I could share code with my Node server. In the websocket setup I talk about earlier- the client and server are constantly exchanging messages. The schema of those messages matters a lot, and sharing types (and code) directly between the two implementations helps to facilitate fast development. I’ll be writing another article on this setup soon.

Roughly, the code breakdown looks like this;

/* source tree */

build/*

src/client/*

src/common/*

src/server/*

What about the game?

Implementing a turn-based game is pretty simple. I like to follow this formula:

Model the public game state. (each user’s hand, stack size, status, whose turn it is, etc.) Implement availableActions(gameState) i.e “given the current state, describe the set of legal transitions to the next game state” Implement applyAction(gameState, action) => gameState which applies some action to the gameState and generates a new one. If availableActions(gameState) is the empty set, the game is over.

Now, unit test till exhaustion. Ideally, don’t even implement the core game logic yourself- pull in a third party NLH library. For something like hand-ranking (“straight” vs. “flush”), this is vastly preferred. Look at code coverage tools to make sure you’re 100% covered before moving on from this step.

The Usual WebApp Stuff

Now that you have a fully tested implementation of NLH available, you’ll need to start interacting with clients. Establishing some baseline schema for user tracking (login with Google | Facebook | Discord, display name, balance, authToken). Spin up a web UI in React to wrap this together, and connect your client to the server via WebSockets. Heroku supports WebSockets, but you’ll need to check with your cloud provider to make sure that the server can accept (and maintain) these connections. Heroku, for instance, demands that you send some Keep Alive packet to prevent termination of the connection.

You can test a lot of this locally, if you’re crafty. I only moved onto my cloud provider when I was ready to meaningfully test multiplayer gameplay.

A Note On Local Testing

For every piece of the napkin diagram we drew, we made the deployment and testing of the app just a bit more complicated. To locally test my poker app, I needed to spin up mongodb and redis . I frequently ran into issues with mongo not closing correctly, which slowed down my overall developer speed.

Here are some tips that made local testing better:

You’ll want to establish a pipeline to seed data into your databases, so that you don’t need to make an account every time.

If you bring more developers onboard, you’ll need to think about Mac+Windows. When I tried to share my project with others, many of the scripts I had written were unix only. Think about Windows contributors too! (in particular- mongodb doesn’t play nicely with WSL)

Linting is important. Lint early, lint often. Restarting your app can be expensive, but linting isn’t. I set up tslint to keep my project in good shape. Make Linting a part of your testing infrastructure!

The Long Road / Maintenance

After finding some bored friends who also wanted to work on this project, I setup a few more things that I think are valuable to consider.

Integration tests that spin up your app, access an endpoint and verify state in the database. Check out jest for this. Branch Standards for your Github. Offer a master branch for general development, and a release branch to deploy your web app from. Run your tests consistently on master (via Jenkins CI), and only merge master into release if tests are passing. Plugins are available for Github that will automatically file tasks for TODO comments in your code. This is a great way to help bring on new contributors by pointing them toward these areas that need love.

Building The Site

While my build process is fully described within the Makefile of the repository I’ve linked to, I can offer a few thoughts on how things are setup.

I build the server and client in parallel, using Make’s -j flag for threading. Each project has its own TypeScript config, and produces a series of .js files in the output. The client’s React setup is responsible for bundling all of the needed Typescript into a series of assets to be hosted by the server.

Once all of this is compiled, I arrange everything in an output resources folder, which the server is started from. Note that resources is checked in to the repository for simplicity, requiring only a git clone and make setup && make local to run the server. Since tests are run against the contents of resources , we can be confident that starting the server will achieve the same behavior.

Sharing Code (TS Node + React)

In order to get my client and server to share Typescript definitions, I used a bit of a hack. My shared folder is symbolically linked between the client and server. Were I to continue working on this project this would definitely not be OK, but at the time this unblocked me. TypeScript’s config should allow for the paths to be ‘searched’ implicitly.

Putting It All Together

After a late night, I finally deployed the app to Heroku. That night we ran a few hands, saw chips exchanged, and had a good time. After signing off, jacknine (the poker site) sat in my private Github library for over a year — until now! The UI could use some work, and there are bugs, but it mostly works.

Enjoy! Let me know if you have any questions.