A few months ago I came across an intriguing GitHub repo:
xiegeo / commit-sudoku
Accepting pull requests to collectively finish a Sudoku puzzle.
commit-sudoku
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>
<td>7
<td>
<td>9
<td>
<td>
<td>
<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:
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)