Monday, January 31, 2022

Dynamic web page background with the Paint API

To spruce up the look of Swătch, I'm investigating animating a background for the game. This turns out to be a bit less trivial than I'd hoped; there are a couple of options, but none of them at this time are great. In this post, I'll describe some options I considered and which I settled on.

A pattern of regular hexagons; egshell fill with white borders
The bestagon

CSS Painting API

A relatively new proposal is the CSS Painting API. This API lets you "hook" the CSS rendering pipeline via code similar to a web-worker. When CSS demands an image to render, it will delegate to your painter with a 2D render context, height and width of the space to draw, and any additional CSS properties you declare that your painter receives; you then render into the provided context (using JavaScript code) and CSS will use the result as an image.

This API is powerful, but hooking into it required some adjustment. To build a web worker, I had to modify my webpack configuration to emit two output files; webpack supports this via modifying the entrypoint to use object syntax and then modifying the output rule to look like [name].js to template-in the object name from the entrypoints. But I then had to fight TypeScript because the Painting API isn't in the dom types yet; I fought with webpack trying to use .d.ts files (tricky; webpack hates this) but finally gave up and wrote the paint worker in plain ol' JavaScript.

Pros:

  • Anything you can do in canvas rendering (kinda) you can do to the background of your object
  • Uses regular CSS, so tools like animations work

Cons:

  • Fighting with TypeScript to use an API that isn't typed yet
  • Modify webpack to emit a web worker
  • (the big one) This API not supported by Firefox of Safari yet

Deciding I didn't want to limit my new feature to only Chrome users, I sought an alternative.

toDataURL and canvas

It's possible to have a bare canvas object emit its image buffer via a data URL. Once you've done that, you can set the background image of an element to that data URL, and you're all set. A bit inefficient, but it works. There's a great tutorial post here describing the process.

Armed with this approach, it was relatively straightforward to build a small hexagon background on a 100 x (height of the hexagon) rectangle.

Pros:

  • Supported in all modern browsers
  • Works with background-repeat CSS, so constructing and setting a small image automatically gives you tesselation

Cons:

  • Will this be fast enough to do simple animations? Not yet sure

Armed with this approach, I put together a simple demo on top of my testbench to prove out the idea.

index.ts:

const SEGMENT = 100 / 6;
const HEX_HEIGHT_WIDTH_RATIO = 1.1547005;

function go() {
    const canvas = document.createElement('canvas');
    canvas.width = 100;
    const HEIGHT = SEGMENT * 4 / HEX_HEIGHT_WIDTH_RATIO;
    canvas.height = HEIGHT;
    const ctx = canvas.getContext('2d');
    if (!ctx) {
        return;
    }

    ctx.fillStyle = '#EEEEDD';
    ctx.fillRect(0,0,100,HEIGHT);


    ctx.strokeStyle= "white";
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, HEIGHT / 2);
    ctx.lineTo(SEGMENT, HEIGHT / 2);
    ctx.lineTo(2 * SEGMENT, 0);
    ctx.lineTo(4 * SEGMENT, 0);
    ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
    ctx.lineTo(100, HEIGHT / 2);
    ctx.moveTo(SEGMENT, HEIGHT / 2);
    ctx.lineTo(2 * SEGMENT, HEIGHT);
    ctx.lineTo(4 * SEGMENT, HEIGHT);
    ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
    ctx.stroke();

    const body = document.getElementsByTagName('body').item(0);
    if (!body) {
        return;
    }
    body.style.background=`url(${canvas.toDataURL()})`;
}

go();

The result generated the pattern at the top of this post; I'm not at all unhappy with it!

Monday, January 24, 2022

Setting up a test bench for TypeScript compiled client code with Node.js (why are things like this?)

My first computer was an Apple ][c owned by my parents. In that environment, booting the machine with no disk in the drive would load a working development environment in the form of an Applesoft BASIC REPL.

Sometimes, when I spend an hour configuring a development environment for testing a web application, I miss those days.

I've uploaded a bare-bones testbench to GitHub for playing with TypeScript code compiled to run in a browser client. To do so, I set up the following dependencies:

  • typescript
  • webpack (collects JavaScript into one file)
  • ts-loader (lets webpack drive TypeScript compilation)
  • webpack-cli (command-line tools to drive webpack)
  • webpack-dev-server (server to watch system changes)

Once these were installed, I had to set up with a tsconfig.json file for TypeScript and a webpack.config.js file for webpack. It took a little while to pull all the pieces together, but the nice thing is that now they're set up and I don't have to do it again.

Why are things like this?

When I look back on how programming was when I started vs. now, I get nostalgic from time-to-time about how things were simpler. We just turned the computer on and coded it! What happened?

More flexibility means more complexity

The computer I coded on when I was six did exactly one thing at a time. Meanwhile, as I type this, I have my testbench open in another window, documentation for npm in a second window, my blog statistics open in a third window... Modern computers are simply more flexible, and there does exist a strong correlation between flexibility and complexity.

In a real sense, the art of software engineering isn't creating features, it's denying options. The most featureful computer is a machine exposing a REPL. You can do anything it can do! You just have to write the code! But we've built an entire global industry around paring down the set of possible things a computer could do to the subset people actually want to do (and in so doing, surfacing those features behind a few buttons at the cost of pushing every other feature of the machine deeper into a tree of options). An Apple ][c with no disk in it "knew" I wanted to program something in BASIC when I turned it on; a modern computer has no "idea" what the user wants.

Since building a tiny web server with TypeScript and Node.js is as likely to be what I want to do as checking my bank account, there is necessarily more complexity to make it happen.

Embedded development isn't like desktop development

Since I'm working on "just some browser stuff," it's easy to fall into the trap of thinking that what I'm doing is simple. Browsers are easy to code; pop open one text file and start writing.

That's true... If you want to code in HTML and JavaScript. But add TypeScript to the mix and, oops, I'm not doing "native" coding anymore. Now I need an infrastructure to deal with the impedance mismatch between the fact that the browser only understands JavaScript and HTML and the fact that the only context I'm willing to code in bare JavaScript anymore is if a child's life is on the line (and even then, it'd better be a child I like).

Looked at in this way, writing web comtent in TypeScript isn't even just compiled; it's more like embedded development: we are writing code in a language not directly supported by the embedding target (the browser) and we need to update the target's view of the code when it changes (recompile and put the data where the browser can see it). In that sense, writing web content with TypeScript looks a lot more like writing code against a microcontroller (albeit a fancy microcontroller run in a virtual machine on my desktop) than writing a BASIC program. Some complexity is to be expected. 100% of the dependencies I installed were to support automatic recompilation and hosting of changes to the code without having to stop and re-start a server.

The real problem is discoverability

At the end of the day, none of these problems would be problems if I already knew what I was doing. This testbench isn't the first one people have written (nor will it be the last). But currently, a search on GitHub for typescript node testbench only returns my code repository!

A view of the GitHub search interface, showing typescript-node-testbench as the only search result for "typescript node testbench"
This can't be true.

I can hope that someone sees this blog post and that the existence of my project somehow gets into StackOverflow or Google search results, but ultimately, this is the real complexity of modern programming: when you want to do something relatively simple, how do you get started? In the '80s, I could start by powering on the computer. How do I start now? typescript node getting started returns several more results, but none quite what I'd want (most of them add more complexity than I need; many are descriptions of how to solve the problem, not workable starter code).

This problem isn't trivial to solve; computing moves fast these days. Several of the technologies I'm using for this project didn't exist ten years ago. But putting starter frameworks in a place people can find them (and perhaps, even, creating a language where people can describe what they want) still feels like a problem in need of a better solution.

We've made incredibly flexible machines, and our ability to talk about them hasn't caught up to the inherent complexity of that flexibility.

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!