Monday, January 17, 2022

I improved boardgame.io (a tiny bit)

The boardgame.io logo

I've been using the boardgame.io framework to create a multiplayer game as of late. It's been going very well, but I did trip over a few quirks of the library. One in particular, I was able to chase down and submit a patch to tweak, which was graciously accepted by the framework maintainers! I thought I'd record just a bit of how that went.

The issue

The library includes a lobby management system that lets the game code host multiple simultaneous games on the same server. There is a convenient Lobby React component that exposes this feature to the client and lets a player choose their name and create / join games. One issue with the component is that it polls the server every two seconds to get the list of games, and this polling continues even when the player is playing a game. It's unnecessary traffic; anecdotally, having the UI open for a half hour consumed about 1MB of data.

I set about adjusting this. After forking the project and downloading a copy of the source from GitHub, I let it build and ran the unit tests to confirm I had a good development environment. One convenient thing about Node.js projects is that they're very consistent in this regard; npm is a fairly mature package management solution, and a well-constructed Node package describes both how to build a package for production and how to build an environment to develop it, so you generally have what you need to do testing after one npm ci run. The developers have also helpfully included contribution guidelines, which make it much easier to know how to develop changes to the framework.

The obvious place to start was the component itself, and after a bit of fishing I found the React component definition, containing functions _startRefreshInterval and _clearRefreshInterval. Sure enough, these functions got called at component mount and unmount, but not when the component transitioned from listing games to playing a game (or from choosing a player's name to listing games; it would start polling before the list was displayed). I was able to make some tweaks and build out a unit test, and then I set about manual end-to-end testing. This proved very valuable; there were a couple corner cases in my initial attempt that manual testing caught before I pushed a bad patch up to the maintainers.

To make clear my intention, I filed an issue against the main project to give some context on why I was making a change. Even when I think the change is self-explanatory, I find filing an issue to be a good step when proposing a change to someone's GitHub project; it makes it clear why the change is happening and serves as a place for others to comment on the problem itself, not just the proposed fix. I like to follow the "What I did / expected / observed" pattern (where appropriate) to make clear why I think something needs to be changed.

As per the contribution guidelines, I pushed my changes to a new branch and created a pull request to propose the change. I expected to go a couple rounds with the maintainers on getting it just right (in particular, I didn't know how they'd feel about what I'd done with the unit tests), but to my surprise they were happy with it and accepted it on the first pass! The change is now in the latest minor revision, so hopefully others will benefit from fewer poll requests while their games are running.

I haven't done many pull requests on other people's GitHub projects, and this was the smoothest I've experienced. I credit part of it to a lot more practice in my day job on the flow of the process. Hopefully, this write-up can serve as a template for others to make changes they hope to see in code they use. Things that really worked in my favor here, I think:

  • This project has a well-defined README, community standards, and contribution guidelines. Read all of those.
  • Node.js makes working on somoene else's code very, very simple. It's probably my favorite package management system nowadays, and that's really saying something; I'd pretty much written off package management as unsolvable, and npm is proving me wrong.
  • Full unit test coverage never hurts (nor does it hurt that the maintainer put a coverage-check infrastructure in!).
  • Detailed and followable end-to-end testing that checked on corner cases increased confidence in the change (and found some bugs). Do this. Think about corner cases.
  • If you take the time as a maintainer to make changes easy and are responsive to changes, you give submitters a positive experience and they're likely to come back!

Monday, January 10, 2022

Making a turn-based game: Heroku is unfairly good, yo

The Heroku logo


So over the course of several weeks (and several blog posts), I've created a multiplayer turn-based color-guessing game hosted on DreamHost. I've been pretty happy with it, but there's still a couple annoying quirks in the situation:

  • The server is HTTP only
  • Boardgame.io supports websockets, but attempting to use them fails

Both of these are related to my host: I have an old shared-hosting solution on DreamHost running Passenger Phusion, which makes it slightly trickier to support these features. It doesn't appear to e impossible: there is, for example, some discussion and a code example for setting up both SSL and websockets on DreamHost. But I think there's a simpler approach that I wanted to try first.

Friends, Heroku is unfairly good.

Setting up Swătch on Heroku

Heroku is a "Cloud Platform as a Service" (PaaS) that lets you very quickly host an app using one of several formats and frameworks, including NodeJS. Once configured, deploying changes is as easy as git push to a relevant target.

The boardgame.io documentation includes some pretty clear directions for setting up a Heroku instance to host a game... If you already know how Heroku works. This was my first Heroku app, so I took a few more steps to get there.

For starters, I set up two new apps on Heroku (swatchgame-dev and swatchgame) and arranged them in a pipeline so that I can get one-click promotion of staging candidates at swatchgame-dev to production at swatchgame. "Every project has a development environment... Some are privileged enough to have a production environment separate from the development environment." I then installed the Heroku CLI tools on my work machine (sudo snap install --classic heroku). That gave me the tooling to configure my git repo to work with Heroku (heroku git:remote -a swatchgame-dev, which set up a remote that would push to my Heroku app).

To prepare my app to work on Heroku, I only needed to do a couple of things:

  1. In package.json, clean up my build rule so it built both the client and the server code:
    "build": "npm run build-server && npm run build-client"
  2. Add a declaration to package.json to let Heroku know what Node version is expected:
    "engines": { "node": "12.22.7" }
  3. Add a Procfile to the root of my project that told Heroku what to do to launch the project once it was installed. The file is a single line, web: node build/app.js, which tells Heroku to set up handling web connections by running the app.
  4. Make the app read from a PORT environment variable to check what port to bind to. Heroku sets that variable to let Node.js apps know what port to listen on for incoming connections. This required only a few tweaks to the setup logic in the server's app.ts file

The necessary code change was very simple:

import 'process';
. . .
const MAYBE_PORT = Number(process.env.PORT);

const PORT = isNaN(MAYBE_PORT) ? 8000 : MAYBE_PORT;

server.run(PORT, () => {
    console.log('server on');
}); 

Once all that was done, I simply had to push to Heroku. Heroku is watching for changes to only a couple of branches (master or main), so I just had to make sure my push matched the correct branch name (since I develop in a local develop branch):

git push heroku develop:main

I then visited https://swatchgame-dev.herokuapp.com/ and... It just worked! No further changes needed. Both HTTPS access and websockets are handled via Heroku's default configurations.

Seriously, Heroku made this ridiculously easy, and it's free to use for a project this small. I couldn't recommend anything else at this juncture.

The game is now playable at https://swatchgame.herokuapp.com/... Try it out!

Monday, January 3, 2022

Making a turn-based game: Don't prematurely optimize, and different types of turns, and code available on GitHub :)

One of the things I wanted to add to Swătch was the ability to play different types of games. In addition to the initial game of "choose the shade of a color based on its name," I wanted to have the following:

  • Given a shade and several options, pick the correct name
  • "Trick" mode: each player makes up a name for a displayed color shade. Then, they're shown the names provided by other players in addition to the correct name. Choose which name was the correct name (players also get points for tricking the other players into choosing their name).

Supporting this atop boardgame.io proved extremely possible, with just a bit of work.

But first, a digression...

Don't prematurely optimize!

To support the "choose name based on shade" mode, I needed to extend the JSON file listing the set of all colors to include references to several nearby colors in the dataset. There are several clever approaches to this, but I chose the simplest one: for every color, find the distance to every other color and assign the N nearest as nearby colors. This is an O(n2) algorithm; pretty inefficient.

But since it was easy to code, I ran it anyway. On a 1,500 line input, it took about 0.1 seconds.

Friends, computers are fast nowadays. For heavy-lifting computation, don't burn time on being clever when you can use the sledgehammer of modern clock speeds.

Different types of rounds

Boardgame.io provides a fairly robust framework for distinct game turns, as well as describing what moves are valid in different stages of play. The framework also supports hidden state (both global and player-specific) for each player. Originally, I constructed the game with just a single type of turn, the guess-the-shade-from-the-name turn. To support multiple different types of turns, I made the following changes:

  1. Added a Round interface, which described a round of play by providing the following methods:
    • initState: Prep the public and private state for the round (set to initial, empty values)
    • onBegin: Begin the round, choose hidden values (such as what color to guess), set up player state.
    • scoreRound: Calculate and update scores and assign "best" (and possibly "second best") answers for later display.
    • buildPreviousRound: Every round, upon conclusion, bundles its results into a previousRound data structure used to describe how the round went to the players. This is the method that turns the round state into that structure.
    • moves: A map from the valid moves in this round to the code to execute them
    • getPlayerState: Accessor for the per-player state for this round.
    • getPublicState: Accessor for the public state for this round
    • getLastRoundState: Accessor for the previous-round state for this round
  2. With the interface in place, I moved around the existing Context state to carry different types of context depending on the current round (and reworked the client-side code to pull from those new contexts and show a specific view based on what the current round was).
  3. Now, I could refactor the existing turn logic into a GuessShadeRound implementation and generalize the boardgame.io-provided onBegin and onEnd methods to select a round to play (with only one option currently available) and score the round when everyone had taken a turn. At this point, the refactor could be tested.
  4. I built out a Rounds.ts file to consolidate the separate round implementations.
  5. With all of that out of the way, I could write three more Rounds:
    • GuessNameRound, which shows a shade and asks players to select the right name for the shade.
    • A pair of sibling rounds: MakeUpNameRound and GuessMadeUpNameRound. These are special-cased: instead of the turn ending when MakeUpNameRound completes, it transitions directly into the GuessMadeUpNameRound and calls that round's initState to prep up the round for it.

This is all tough to follow from description alone, which is why I've made all the code available on GitHub to see. Feel free to run it locally and give it a try!

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

Boardgame.io 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');
server.app.use(async (ctx, next) => {
    console.log(`requesting ${ctx.request.path}`);
    await next();
});
server.app.use(serve((frontendJsPath)));
server.app.use(serve((frontendPublicPath)));

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.

Webpack

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

Boardgame.io 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 http://swatch.fixermark.com; 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.

Monday, December 13, 2021

A Whirligig for a play

Taking a short break from talking about Boardgame.io for a project I took on this weekend.

My wife is in a play this holiday season, and one of the plot points is a kid struggling with a science fair project getting a little help from the main character. I went ahead and did some prop work for the before-and-after on the project, and I'm pretty happy with how it turned out.

Preparation

Amanda sourced several STEM kits from Sntieecr for raw materials so we could make the prop dynamic. They were pretty good, but they don't include any structural elements. I had a few yard stakes in the basement that I figured had enough bulk to serve. When it comes to woodworking, I'm absolutely amateur; I don't even have a jigsaw or a workbench. But I was able to jury-rig a bench with random stuff in the basement, and with some grunting with a saw I was able to segment a few yard stakes into 8" and 10" lengths.
A jury-rigged workbench: two folding chairs supporting a single shelf from an old Borders bookshelf. On the shelf, my drill kit, saw, the wood, and some clamps.
Literal basement-tier work

I figured I could make a sufficiently-sturdy shape with a 3-4-5 right triangle and a cross span to keep the whole thing from tipping over. Yes, my understanding of mechanical stability pretty much tops out at this.

Against the backdrop of a stack of three cut lawn spikes, "The plan": a rough sketch of a windmill.


The work

One thing I was concerned about was that without a jigsaw, cutting an angle into the top of the diagonal was going to be a real chore. Then I remembered that since I was using yard stakes, the original manufacturer had tapered the end for me. Thanks, industrial manufacturing!

Two trusses, one including a diagonal brace, the second with the diagonal brace pulled away.



With a "before" and "after" truss finished, I was ready to put the STEM kit on them.

On a large table, the two trusses and an open bag of small parts: motors, switches, plastic fan blades that fit to the motors, battery cases, and a large assortment of wires.


Here I hit a small snag: after affixing one of the light bulb sockets, it came apart when I screwed the light bulb in! It turns out the whole affair (socket threading, anode terminal, cathode terminal, and a paper insulator between) was held together with a single screw, and it had shipped loose; screwing the lightbulb in unscrewed te screw and dropped all the parts out of the socket. Once I figured out what had gone wrong, it was pretty straightforward to put together.

The kit worked great once assembled, and I was excited to see it in action.


The last step was to bond the gadgets to a base board for easy transport, I was worried about a screw sunk into the stakes on the thin side splitting the stakes. So I used hot glue. A lot of hot glue.

Close-up on the joint showing hot glue
There are problems hot glue cannot solve.
Someday I'll meet one.

The result

Overall, I think they turned out okay! I hope they're robust enough for handling during the show, but I think they'll be fun!

The "before" whirligig, with several small parts laying around it indicating it's half-done.


The "after" whirligig, notably Christmas-themed.

Monday, December 6, 2021

Making a turn-based game: Getting started with boardgame.io

For my game, I went looking for a framework that would handle some of the boring parts and free me up to focus on the game mechanics and logic. It didn't actually take much hunting to stumble upon boardgame.io, which I was surprised to discover handled just about everything I wanted.

In this post, I'll talk a bit about my choice of engine and the steps I took to get started.

Architecture

The boardgame.io framework has several features I appreciate:

Language and libraries

  • Designed to run on NodeJS, which I'm already familiar with
  • Includes React components
  • Includes TypeScript type bindings

Features

  • State synchronization between server and clients
  • Client-side prediction of next move on public state, to speed up the experience
  • Automated client information hiding
  • Game state machine library
  • Debugging UI
  • Integration with a couple of backing stores
  • Rudimentary multi-game server and client lobby

The framework was designed with Heroku in mind, but I was able to get it working on a shared DreamHost server. For the remainder of this post, I'll go into that process (which involved a lot of dead-ends, but eventually worked!).

Getting started with a boardgame.io project

For this project, I set up a pretty standard Node project:

  • installed nvm using the wget directions
  • nvm install 12.22.7 to sync with version required by DreamHost for compatibility with Passenger Phusion
  • configured the project to use TypeScript
  • Started setting up the boardgame.io tic-tac-toe tutorial

By the end of this stage, I had the following files set up:

tsconfig.json ``` { "compilerOptions": { /* Language and Environment / "target": "es6", / Set the JavaScript language version for emitted JavaScript and include compatible library declarations. / "lib": ["es2015"], / Specify a set of bundled library declaration files that describe the target runtime environment. */

/* Modules */
"module": "commonjs",                                /* Specify what module code is generated. */
"rootDir": "src",                                    /* Specify the root folder within your source files. */
"moduleResolution": "node",                          /* Specify how TypeScript looks up a file from a given module specifier. */

/* Emit */
"outDir": "build",                                   /* Specify an output folder for all emitted files. */

/* Interop Constraints */
"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": false,           /* Ensure that casing is correct in imports. */

/* Type Checking */
"strict": true,                                      /* Enable all strict type-checking options. */

/* Completeness */
 "skipLibCheck": true                                /* Skip type checking all .d.ts files. */

} } ```

tslint.json


{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "no-console": false
    },
    "rulesDirectory": []
}

One note about the boardgame.io tutorials: they'll mention using parcel-bundler, but it's deprecated for parcel.

Next on the list was to set up React.

  • npm install react (which gave me v17.0.2)
  • npm i --save-dev @types/react
  • add "jsx": "react" to compilerOptions in tsconfig.json
  • npm install react-dom
  • add dom to lib in compilerOptions
  • add "include": ["./src/*"] as a peer to compilerOptions (note: src/* is not the same and will result in failure to find modules)

This gets us halfway to a working solution, but to serve from DreamHost, we'll need to have a JavaScript program that Node.JS can run and a program (likely one JS file) that can be vended to the client. Configuring that is the next step.

Monday, November 29, 2021

Making a turn-based game: setting up DreamHost for NodeJS

Recently, I've taken an idea I started prototyping on boardgamearena and moved it to my personal server. it's been an exciting project so far, and I'm getting close to showing it off widely.

I'm going to do a few posts talking through the process of getting the game working. This one will go into getting DreamHost to run a Node server.

Using boardgame.io with DreamHost

DreamHost uses a shared Phusion Passenger instance to host web apps, which is compatible with NodeJS apps. Details of configuration are helpfully provided in their documentation and an associated example. To start, I set up a new subdomain and made sure to enable Passenger on it.

DreamHost expects the NodeJS app you want to serve to exist as an app.js file at the root of the domain directory. My first attempt was to set up one of the example scripts, but it immediately crashed. Hand-running on port :8888 by executing node app.js in an SSH terminal succeeded. I followed some of the troubleshooting advice (chmod 755 app.js, then mkdir tmp; touch tmp/restart.txt to force Passenger to restart its instance of my server). No luck, so I modified the .htaccess file at the domain root to offer some more details:

PassengerNodejs /home/myusername/.nvm/versions/node/v12.22.7/bin/node PassengerFriendlyErrorPages on

Adding PassengerFriendlyErrorPages helped here; turns out I was seeing

*** ERROR ***: Cannot execute /usr/bin/node: No such file or directory (2)]

At this point, I got a little stuck and filed a support ticket to DreamHost. It didn't take them more than an hour to respond, and the solution was straightforward: turns out I was the first person trying to use Node on this shared server, and the Passenger Phusion server itself needed a restart. Heh.

After the restart, I got a successful Hello World message. next step was to begin setting up my boardgame.io project on my local machine.

We'll go into that next week.