The Game Itself
Poker poses a challenge for web apps because it has user to user, turn-based interaction and requires the server to selectively share state with players. I implemented a simple version of Poker, called High Card, in which each player gets one card, no community cards, and the player with the highest card wins. There is one round of betting, and players can re-raise each other indefinitely provided sufficient funds. In the video below, you’ll see two players login, generate some fake chips, one of them creates a game, both join that game, and then they play a few rounds of High Card against each other.
Authentication and Client/Server Communication
During account creation, passwords are stored as bcrypted hashes in the database, so passwords are not stored as plaintext and frequency analysis attacks on passwords in the database are not possible. When a user logs in, the input password is bcrypted and compared against the stored hash. If it matches, a unique session id is generated and stored in the database with that account and the server returns the session as cookie to the client. The client attaches that cookie to all subsequent HTTP and websocket requests, which is used to authenticate the user. Page loads, account creation, and user login requests are done via HTTP, but all other requests are handled with web sockets. The primary reason for this is to allow the server to push events to clients.
I wanted to experiment with React and Redux for this project.
React is a clientside view layer that lets you design stateless web components that naively display data when told.
Redux is a clientside controller layer that defines a one way stream of events into the view layer.
One thing I came to like about Redux is that all actions, whether user interaction or server events, simply drop new items into the event stream, and are treated equally.
In Redux, all actions have a type, and are ingested into the Redux store, which uses the action’s type to route to the appropriate handler (known as a reducer in Redux terminology).
The logic to handle a user clicking a button is handled the same way as the server signaling a state change to the client.
This design pattern is useful for Poker App because almost every action a player takes needs to register in every other player’s browsers.
For example, when player one clicks “check”, that click event 1) triggers player one’s browser to show that it is player two’s turn, and 2) tells the server to notify all players (including player one) of this event, which causes their browsers to perform step 1.
The user interface feel snappy because updates happen clientside, without waiting for a roundtrip to the server.
If player one does something that isn’t allowed, the server will eventually respond with the true game state, correcting player one’s browser, and in that case, the event won’t propagate to other users. I chose to leverage Redux’s action type serverside by requiring that all websocket requests had this field as well.
The client code is written using ES5, and the React components are using .jsx syntax. I use webpack to leverage require statements, which allows the js code to be organized into a practical directory structure, and webpack also compiles the project into a single js file to ship client side. I had started this project using browserify, but quickly came to prefer webpack’s ability to generate debug and/or minified versions of the client code. For UI styling, I’m using a project called Semantic-UI. I like the number of theming options and ease of switching between themes that Semantic-UI offers, though I think that the project is a bit heavyweight for my purposes. I would probably explore other options (like maybe react bootstrap), if I were to redo this project.
The server is written entirely in Go, and uses MongoDB for persistent storage. I’m using a minimalist web framework called Echo, which boasts speed, supports middleware and websockets, and is context based. I chose MongoDB because I wanted the option to support many game types (besides High Card), but store each game’s data in a single table The schemaless nature and community backing made Mongo a reasonable choice for this use case.
As a web engineer, I spend considerable time building HTTP APIs, where handlers are responsible for validating input, coordinating model calls, and determining appropriate client responses. Therefore, I sought to replicate this pattern with websockets. To this end, I built a middleware that parsed out the action type from the websocket request, since I chose to require every websocket message have a type. Then in the web server router, I created a mapping of action type to websocket handler, which has a very similar feel as the mapping between an HTTP route and its handler. The main difference between an HTTP handler and a websocket handler is that HTTP handlers take a context as input, whereas websocket handlers take a websocket message. Though, each websocket message has a context property, which makes the two functionally equivalent. From there on, the websocket handlers behave the same way as HTTP handlers. The context object in a websocket message stores a reference to the websocket connection manager. Whenever a websocket handler needs to communicate an event clientside, it uses the connection manager to send a Redux message with an action type and the appropriate payload.
For High Card, there is a websocket handler per action a player can take, check, bet, re-raise, fold, and start hand. When start hand is issued, a card deck object is instantiated, and cards are doled out to players. The High Card model code has logic to inspect the recipient of a message and strip card data from that message if the recipient and card owner don’t match. This means that player one’s card data is never sent to player two’s client until a final state is reached and the winner needs to be determined.
I wrote integration tests using CucumberJS. These tests are focused on simulating end user behavior via automated browser testing using selenium. There is a docker container running the selenium hub and another container running a selenium chrome node. I can run tests where multiple players interact with each other by establishing two chrome connections and simulating each player’s behavior. Some of these tests take screen shots using a project called webdrivercss, and run pixel diffs on previous iterations of the test with the webdrivercss admin panel. These tests are mostly a proof of concept, rather than rigorously testing Poker App. The webdrivercss tests are useful to see when a webpage has changed, at a pixel level. The admin panel project provides tools to promote a pixel changeset to a new master state, or alert you when a change occurs erroneously.
The server application, Mongo instance, selenium hub, and selenium Chrome node are each running in their own Docker containers.
I use docker-compose to link Mongo to the server and the selenium nodes together.
There is a master config file for the server, which defines the hostname, database info, and some other variables.
After any changes are made to the client code, I need to run
The server binary and config file are mounted in the server Docker container, and running
docker-compose up -d will start the server.