Monday, December 27, 2021

Making a turn-based game: A game server on DreamHost

The next step in setting up our turn-based game is prepping up our server to do double-duty: it needs to both vend static HTML content and handle dynamic client-server traffic to run the game logic. To that end, we need to extend the initial configuration to support both.

Koa server integrates with the koa framework to serve HTML. It's a relatively straightforward web server that supports a static server (koa-static); the server is even instrumented with the npm debug library and can give debug logging output if the DEBUG=koa-static env var is set.

Armed with this knowledge, I can improve the server.js file to vend static content.

const frontendJsPath = path.resolve(__dirname, '..');
const frontendPublicPath = path.resolve(__dirname, '../../public'); (ctx, next) => {
    console.log(`requesting ${ctx.request.path}`);
    await next();

Now we're making progress, but the client code doesn't work because the browser can't import npm modules. It's time to bundle up the client-side code into something a regular JS browser can handle.


Being already familiar with webpack, I chose to grab that as my weapon of choice for generating browser-compatible code. First step is to get it into the project:

npm install --save-dev webpack
npm install --save-dev ts-loader

I set up the webpack config based on the guide and changed my build rule to tsc && webpack. My output is now public/bundle.js instead of build.

At this point, I realized I'd need two different configs for client and server code. I can do this with two tsconfig.json files and the tsc -p build option to select which one to build. With a little reorganizing of my code (client-specific in "client", server-specific in "server", and common in "game"), I can now generate two build artifacts: a server that can run in Node, and a client that the server vends to a browser that connects to it.

After uploading all that content (the build artifacts and the node_modules directory) and giving it a run, I had a successful instance of the game running from my server.

The Swatch user interface, showing a color to name

Now to set up a lobby.

Supporting a lobby provides a rudimentary lobby server that makes it easy to create and connect to games (just replace the top-level game object in your code with a Lobby client). Not quite documented is that lobby also assumes that HTML components that are class hidden will be hidden from user view (display: none css rule works fine). At this point, I also discovered that the default config rules for caching on my Dreamhost server didn't work well; every piece of GET content was served with a Cache-Control: max-age=172800. I added some cache-breaking to the .htaccess file:

Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0

Hiding colors from end-users

One last bit of housekeeping at this stage: the color database is a JSON object that gets read into memory, but it should live only in the server memory; clients should not get a whole copy of it. To that end, I stored the colors in private/colors.json and used Node to fetch them (require('fs'), require('path'), dynamically load the file at boot time). I also had to get those files out of webpack's field of view so it didn't try (and fail) to bind fs and path into browser code. The following got added to webpack.config.js:

resolve: {
  fallback: {
    'fs': false,
    'path': false,

So in the common code, I can check if fs.readFileSync is defined, and if it is not I know I'm in the browser and can avoid trying to load colors.

Give it a try!

The game is up and available for 2 or more players at; feel free to try it and let me know what you think!

Next post, I'll talk a bit about re-architecting the game to support different kinds of rounds.


  1. It's only available over HTTP, not HTTPS, and gets an SNI cert from invalid authority for a default dreamhost cert over HTTPS.

    1. Correct; thank you for catching that. Updated the URL.

  2. Play fails for failure to setup a websockets connection (it is `ws:` not `wss:`). Developer console also shows some complaints from `colorToCode`.

    1. (The "ws:" thing was eliminating one source of possible issues, in light of the previous issue, not saying that this is the bug, sorry)

    2. Updated. There's a bug in single-player I haven't tracked down yet; need to get that chased. It should work for two to eight players.