DEV Community

Anish Karandikar
Anish Karandikar

Posted on • Edited on

Animating a commit based Sudoku game using Puppeteer

A few months ago I came across an intriguing GitHub repo:

GitHub logo xiegeo / commit-sudoku

Accepting pull requests to collectively finish a Sudoku puzzle.

commit-sudoku

Build Status

1 5 7 2 9 6 8 3 4
8 9 2 3 4 5 6 1 7
4 3 6 1 8 7 2 5 9
7 4 1 6 5 2 3 9 8
9 2 8 7 3 4 5 6 1
3 6 5 8 1 9 4 7 2
6 8 4 9 7 3 1 2 5
2 1 9 5 6 8 7 4 3
5 7 3 4 2 1 9 8 6

Rules

There are two types of pull requests, moves and others.

A move must only include filling in one number in one cell in the puzzle.

  • A player can only make at most one move a day.
  • A player can remove any moves made by himself or herself without the above limitation.
  • Moves are accepted in order of first submission.
  • Conflicting moves are skipped, they can be resubmitted…

It said it was "accepting pull requests to collectively finish a Sudoku puzzle". I was fascinated. The idea was simple. The author had set up an empty Sudoku board and anyone could add a number to it as long as it was a valid Sudoku move (& max 1 move/day/author). Pretty soon I found myself sending out PRs - adding a 4 here, or a 7 there.

It was a bit difficult to specify your intended move because the board was represented as a simple HTML table in the repo's README.md file.

<table>
  <tr>
    <td>1
    <td>&nbsp;
    <td>7
    <td>&nbsp;
    <td>9
    <td>&nbsp;
    <td>&nbsp;
    <td>&nbsp;
    <td>4
    ...

So I sent an enhancement PR embedding row numbers in the board - and lo! - not only was the PR accepted, but the author @xiegeo even offered me commit access to the main repo! I was psyched! Soon along with sending out "move" PR's I also started checking and merging PR's sent out by other contributors and doing general housekeeping tasks.

When I went back to the repo, after a longer delay, I noticed that the board was a lot more filled than I remembered. This got me thinking - how had the board evolved over time since the first move? So I decided to try and animate the evolution of the Sudoku board state from commit history.

Step 1: Get a list of commits

First, I needed to get a list of commit SHAs that were made to the repo in reverse (oldest first) order.

const commitSHAs = execSync('git log --reverse --no-merges --format=format:%H')
  .toString().split('\n');

Step 2: Get Sudoku state for each commit

Next, for each commit, I need to extract the state of the board from the repo's README.md file. I used git show to do this.

const readmeContents = execSync(`git show ${commitSHA}:README.md`)
  .toString().split('\n');

Step 3: Render each state in Puppeteer

Now comes the interesting part. I have the state of the board as HTML for each commit that I need to render as a PNG. For this, I used Puppeteer - a high level Node.js API for controlling headless Chrome.

await page.goto(`data:text/HTML,${HTMLHeader} ${tableState}`);
await page.screenshot({ path: `tmp/T${new Date().getTime()}.png` });

Here tableState contains the HTML representing table state retrieved from the README.md file in the previous step. HTMLHeader is a little boilerplate to apply GitHub's style.

data:text/HTML,... is very interesting. It's called a Data URL. Basically you can put any HTML after it and the browser will render it. For example, click on this URL: data:text/HTML,<h1>hello, world!</h1>.

Note: The spec recommends text/html (lowercase) not text/HTML (uppercase), but I was having issues rendering it correctly in this blog. Both seem to work.

Step 4: Create animated GIF

Finally, I used the excellent gifencoder package to generate an animated GIF from the list of PNG files that were generated from the previous step.

pngFileStream(`tmp/T*.png`)
  .pipe(encoder.createWriteStream({ repeat: 0, delay: 200, quality: 50 }))
  .pipe(fs.createWriteStream(outputGIF));

Result

So here is the resulting animation showing how the game played out:

animation

Step 5 (Bonus): Hook up to TravisCI and Surge.sh

I did not want to be running this script manually on my machine on every commit to update the animation. To begin with I had missed a few commits to the repo and that's why I started down this path in the first place. Luckily getting TravisCI to run this script on every commit (to master) was a breeze.

after_success:
  - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then cd animation && npm install && npm run generate; fi

This tells TravisCI to only generate the animation if there's an actual commit to master and if all tests pass (i.e. Sudoku board is in a good state).

Finally, I upload the resulting GIF to Surge.sh - which offers simple static asset hosting - so that it can be showcased in the repo's README.md.

SURGE_LOGIN=xxxxxxx@xxxx.com SURGE_TOKEN=xxxxxxxxxx npx surge folder my-endpoint.surge.sh

And that's it!

There are a few details that were skipped in this post for brevity - but you can dig into the complete code here - including how you can run it locally.

Let me know what you thought of the post!

Cheers!

Top comments (0)