This year I almost missed js13kgames! I was on vacation and came back in the last week of August. Somewhen that week I had the thought to look up, when exactly js13kgames would start this year. Panic mode! It was already running for two weeks! AAAAAHHHH!
Okay, so here's a rundown of what I did in the last two weeks of the game jam:
30th August: Setting up boilerplace code
Over the years I've developed a certain set of tools I use everywhere. The very first step for me is always initialising a
package.json (for NodeJS projects, that is), downloading a fitting file from gitignore.io, choosing a license and creating a minimal README, which lists the name of the project, it's aim and a link to the license.
In the next step I am copying over and adjusting the configs for the tooling I prefer:
- Chai and Mocha (defining it as test run-script)
- Standard (okay, nothing to configure here)
This year I thought about trying out a new bundler called parcel. After all, the project is small enough to test new stuff, isn't it?
So the topic was „offline“. Hmm. What could we play? I'm sure, there are games on npm, but I was looking for a compatible one. After all, I wanted to publish it as free software, so I went for GPL v3+.
In the end I've opted for mastermind with the idea of adding other mini games later on.
Since I was late to the party, I browsed JS13kgames resources for a game engine. Too bad, that most were abandoned. But kontra looked interesting. Alternatively I'd have tested Ga. Something modular, please.
Given my choice of license, I went to OpenGameArt to look for assets and found an image of a Switch. So I went ahead and looked into how I could draw it on a
31st August: Looking for a game idea
Okay, so we had the basics in place, what next? I was looking for a name and was browsing commitstrip (as I do several times a week :-)). The strip clicked with me. AOL. An Offline Life. So I took some notes about what the background story of the game should be. Hopefully I would find time to implement it, but it turned out to not be the case. More about that later.
1st September: Fighting tooling
So I decided on developing something with Canvas API but quickly realised, that it would be too complicated to learn all those intricacies myself in that short amount of time. So I tried kontra and it worked okay.
In order to save time later on, I looked into how I could shrink the image for the bundle as well as respect the requirement for the banner of my chosen license.
Turned out to not be flexible enough...
2nd September: Falling back to familiar ground
So the next day I throw out Parcel and went to a bundler I was already familiar with over the last year: Rollup.
But Parcel offered a development server. Something that is out of scope of rollup. Luckily there are enough alternatives. This time I did not used a python server, but went with http-server because of zero config.
I wrote some node scripts to accomplish things next to bundling. For example, the code for checking the file size after zipping is inspired by a js13kgames starter for parcel, but tweaked for my needs.
As mentioned, I turned to kontra to do the Canvas painting, so I've updated my code to render what I already figured out. Since I had no control over the HTML at this point, the
<canvas> was injected on start.
Normally, I would import dependencies and trust Rollup to shake the tree when bundling. I had to learn, that Kontra was meant to be exposed as global. Since Rollup does not check it on bundling, that went smooth. Standard got crazy at me, so I've added a comment to tell it everything's fine.
3rd September: More tooling (Redux and terser)
I learned to love Redux, because is so simple yet powerful. Since I was about to enable offline usage, I should keep my state in one place anyway. Offline as in: Don't start from scratch on refresh of the page.
After adding it, the bundle failed. There was something about not being able to tell the environment it was meant to bundle. I searched the found and quickly found a solution to it.
Since I could now play around with Redux, I refactored my code to keep the state in one place and defined a reducer and store as well. This turned out to be helpful once I throw out Redux :-)
functions. Something, somebody else "solved" using regular expressions.
At this date, I've added two other assets to include later: A spritesheet and a nostalgic soundfile.
4th September: Grokking kontra
For some reason I could not understand, how to actually use kontra together with assets. The problem lie in loading the assets asynchronously, while the rest of code snippets where synchronous.
It took me a bit of research until I figured out how another demo solved it. The trick was to invoke the other functions after the promise of asset loading resolved. So I could write the rest of the code under the premise, that assets were already available.
Since I now not only had to manage the state regarding the mastermind game engine but also of my UI, I introduced redux'
combineReducers. This way, it would nest my state, but also allowed me to tweak them in isolation.
What made me freak out was the sheer amount of updates triggered during the
render tick of kontra. I tried to subscribe during an invocation of the callback and unsubscribe once I finished the rendering. This was quite janky. There must be a better way (and later one I found it!).
Another fight I had with kontra was about rendering the different states of a switch. My sprite had three of them. Since spritesheet didn't exposed the features I needed I wasted my time to do crazy algebra with canvas. Only later on I stumbled upon tilesets, which eased everything. In order to reduce code size I introduced a factory method, which I only had to tell, where the image should appear.
The DevTools plugin failed for a reason, though. If you look closely you'll notice, to add some lines to kick it off.
6th September: It's too big!
Redux is too big. Namely it ships features (and
Errors), which I couldn't eliminate. I need something similiar, but smaller. Sadly, most solutions are tied to React. But I was lucky enough to discover nation. Yes, it lacked reducers, but it gave me most of what I needed (store, composing and state querying). I find its documenting confusing though, when it comes to nested states. To me, it was unclear what exactly is needed to nest states. In the end I composed my global state via destructuring of the sub-state. That is, there is only one store created with
nation. I had read the documentation as if I should create sub-stores as well. Will fill issues against the maintainer to improve the documentation.
At this point I switched to default exports. I vaguely remembered that they are disputed in the community, but my code is structured in a way that I only export one object (most the time a
function) per file, so it's okay.
Also I started painting the second image onto the canvas which yielded to a lot of code duplication and started to frustrate me. There must be an easier way to achieve what I want (I still haven't discovered tilesets). At least it improved readability to introduce symbolic constants for indexing the spritesheet.
As a hack I toggled a flag in the state on every
update of kontra, so that it would trigger a
render call (since that one was wrapped inside a
8th September: Getting closer to a playable state
On this day I've looked into how I could get into something I could actually play.
What nerved me was that I had so few control over the HTML. So I went ahead and fixed that. From my past experience I knew, that I could inline CSS and JS with Pug. It was necessary, since I couldn't load those assets via the file: protocol.
In order to shrink the rendered HTML I looked for a minifier - and found an HTMLMinifier!
Now I know longer had to inject the
<canvas> on runtime!
Another optimisation I took was to flatten the structure of the final output. On GNU/Linux systems, every directory (even empty ones!) take 4 KB, so I was unsure, whether this would affect the zip, too.
This day, I finally discovered tilsets, which eased a hell lot of my problems with rendering the images! By loading an image and describing its content by index, I could abstract away quite a lot of my problems. The best thing: I could put the abstract map into state and this way ease the
render callback by looking up in state first whether there's something new to paint. Otherwise it returned. This way, I haven't to listen for changes anywhere in the state any longer! In the first run I draw a static map, though.
I had quite some fun with GIMP after realising, that I could actually merge my two images into one tileset (plus, the license allowed for modification). What I was quite confused with on pasting the switches into the computer sprite sheet was that the colours vanished. Somewhen I stumbled upon the info that there's an indexed mode which was turned to 8bit with a fixed colour palette. Too bad that some details got lost.
Some challenges were the mismatching tile size and the transparent background of the switches. I "solved" the first one by upscaling (it was too tiny anyway) and the later on by copying the background from various other tiles into the last row, then overlaying it with the switches. If you want to do that on your own, make sure to copy the layer of the original, insert the paste onto a new layer and keep an eye on the order of your layers in the stack. Drop me a line if you want me to explain this more in detail :-)
In order to be friendlier on the eyes, I added some CSS to gave the game a decent background colour and center the
9th September: Map generation
- Increased tileset. Included maze algorithm.
- Path generation algorithm. Playing audio on click.
- Smaller dep.
My number one hindrance on js13kgames submissions are the maps. I still haven't figured out how to draw a map given a decent sets of constraints.
Since a static map looks boring, I want some generator to figure out possible solutions. Since I knew from the past that I always struggle with it, I looked for solutions. During research I discovered, that there are already a dozen approaches to generating a maze. I tried out one package, which promised to be the most flexible on generating a maze, but struggled to express my constraints and the world in a way I could hand over. If you have excellent solutions for this, please, please write me!
This day I started to describe, which tiles can connect to which others. Given that there were already 16 tiles on the map, this yielded 16 * 16 = 256 combinations! Imagine how this affected the size of the bundle. I stopped after starting to describe the third tile this way.
Instead I turned to adding audio. This time I wanted to not add it last minute (white noise was not my best idea...). Turned out it was super simple with kontra. In order to let the user control it, I bound the play to a click listener. But a click listener could only be bound to a sprite, not to a part of a tileset. Solution: Putting transparent sprites over the tileset :-) It took me a short time to learn, how to figure out which clicks are meaningful. Those used to have received a second argument in the callback. I learned that I got register an object as meaningful by explicitely telling kontra to track it for pointer events.
Since I was not happy with maze, I throw away most of the code I wrote for it. Instead I rolled my own solution. This was broken up into smaller chunks. First, I created an empty
Array, which got populated with known tiles. Namely the switches, the computer (the one in the lower right corner), the modem and the servers (which line up on the upper left edge).
I even played around with
Maps, but it turned out, that the index was looked up by reference, if I put an object into it. That is, even if I created another object later on with the same key-values it wouldn't be found. Hence I turned back to
Arrays and relied on the index. I wrote some helper functions to convert the 1D index to 2D coordinates of my map and vice versa.
So in order to check, where I could place a new tile, I had to first verify that it was within some bounds (= the edges of the map) and not already set. The ID of the next tile, that is, the index for the tileset, had to be chosen in a way that it allowed a connection to the current tile. Was quite some juggling here. The next tile could also only be placed in a direction next to the current tile, to which it could connect. Say, I am currently looking at a curve from bottom to left. I could exclude tiles which would be above of right from the tile. Also I could exclude tile IDs which would try to connect to those edges. If that sounds complicated, I can assure you it was. Did I mention that I am not good at map generation?
I eased the lookup of the possible partners though by not explicitely stating which tile can connect to which other, but by merely describing, in which direction it could connect. This way I could look for an intersection between two sets (too bad that I found no way how to use
Sets in a meaningful way here). Since
Arrays are indexed by 0, but kontra's tilesets start at 1 (with 0 being a transparent tile), I had to carry the id instead of relying on the index.
Since I no longer relied on kontra's node_modules, I had to update the dep by using the download function of the project website to tell my game about the new modules it uses (pointer events, for example).
10th September: Memoisation and mangling
- Fixed linting issues.
- Cleanup. Persisting changes. Hydrate on init.
- Started working on algorithm to fill the gaps.
- Visualize bundle
After a short cleanup in which I fixed linting issues, deleted unused files and repaired references of recently renamed files, I continued refactoring my game.
Next in line was to move all constants into a single file to save potential size by eliminating strings. Since I used an object to collect all constants I had to manually mangle the keys as well, since terser couldn't rename them safely. As an upside I was able to modify those constants in one place and have the rest of the game being updated automagically.
Since I was using a serialisable state, I could save the state on each update as stringified JSON object in localStorage. On loading of the game, I've looked up whether there is already a value in localStorage for my game and parsed it. This made me cleaning the localStorage several times during development.
Having things in the state allowed me to introduced difficulty levels. Each time the user solved a combination, I added another switch and rendered new tiles on the map (including tracking them for pointer events).
Since I repeatedly hit the size limit now, I added a plugin to visualise my bundle to see, where there was room for improvement. In the end staring at the result of terser helped me more. But charts are always nice to look at :-)
Since I now had the map in state, I could make the rendering of the tilesets lazy by looking up for existing computations. This wasn't possible in the render callback as I still had to render the transparent sprites.
I managed to have a constant path for the first level (by always picking the first option of a list of possible next tiles). This broke as soon as the user reached the second level, since I'd need to add a splitter in the path. I still have no solution on how I could do this.
For the rest of the map I picked a tile at random (but from the subset of possible tiles). This made people thing my game was a pipe game :-(
12th September: A playable version
I repeatedly hitted the size limit. I tried everything to get the file size down of my code. For example, I enabled minification of my CSS during HTML minfiying. Aliasing kontra by another script, which introduces a global
k. Also I added a small function to alias the response object of the mastermind game engine. Renaming files. Using
#0000 (transparent black) instead of the keyword
case statements with a set of
Errors and logging.
But in the end cutted the audio even further. Speaking of audio... the file was originally an ogg file.
I throw it into Audacity and turned it into a MP3. That didn't cut the file size good enough, though. Then I turned it from stereo to mono. Still not small enough. I cut off most of the parts by selecting the most significant parts with my mouse (click, drag, release mouse, then copy to new track, move to beginning and delete the old one). Too large. Next attempt was too downsample it. It's nostalgic anyway. Below a certain Project Rate Audacity refuses to export the file. You need to adjust it in the dropdown of the lower left corner before exporting. Default is 44100 Hz. I reduced it to 8000 Hz.
In order to make the game playable (the next day was deadline) I focussed on the minimum viable state. In one place I've updated the state, in another I read it back to decide, how to render the tileset. Normally the date for it would have been static, but don't tell kontra ;-)
It was quite tricky to determine, which servers to render. In opposite to the switches, they had no stable order. I want to have them sorted by type: First the servers indicating the correct value and position, then the servers indicating correct value, but wrong position and at the end those indicating neither. The first one were blue and head a small green LED. The second had a whirl and a red LED. The last ones were black.
During all those hacks I introduced new bugs, because I haven't updated all occurences when renaming variables. I quickly spotted the missing parts, though.
Now that I had levels and a playable state I faced the next challenge: How do I tell the user, that she has won?
I tried to render text on top of the canvas, but couldn't see any effect. I guess it was because kontra's
render kicked in and painted over my text. So I applied a hack and showed text below the canvas. While I was it I tried to squeeze some bits by shortly telling a story. Turned out not to include enough hints to make the user understand how to play. Funny note: After I tried to trim the space by 1 byte (yes!) I replaced the "You are" (which occured in both messages: Introduction and Win) by "You're". The bundle turned up to be 4 bytes too large. Why? Because that's how zip (or better: Deflate) work. They are creating a lookup table where every entry gets a number. If that entry appears again, the number will be referenced.
In this phase I implemented the logic for level up and win state. You win when there would be no space left to draw another computer or server. So I hardcoded that treshold.
So how do I tell the browser to show the winning message? By utilising a rarely known fact, that HTML elements with an ID expose a global variable to that element.
Now I looked into whether I would be able to play the game on a mobile viewport by turning on the DevTools.
This was the moment when I almost throw my laptop out of the window. What happened?
I was confused that I saw my clicks triggered twice. So I assumed I introduced another bug somewhen earlier. Naturally I
git reset --hard HEAD~1 my repository to check, whether it happens with an earlier state, too. I went back the work of two or three days. The double-trigger still occured. So I couldn't have introduced it by mistake. Okay,
git pull to fast-forward it.
But I haven't pushed the code yet. That is, there is no remote origin to pull from! PANIC!
After walking out of the room to calm down I had the idea whether other people faced a similiar issue. After all, git is widely used.
After some more researching I learned, that I saw two events. One was
touchstart, the other
mousedown. I filtered for one of them and fixed that issue.
So it was time to finally put the code on GitHub. Luckily there are gh-pages to host them for me. Pushing the code there made me realise that I missed a crucial detail when I wrote the HTML template: the
meta element to make the viewport mobile-friendly!
So another round of tweaking code here and there to make it fit. This time I truncated IDs of HTML elements and CSS colours to compensate.
Testing on Opera turned out another issue if you load the game with its file: protocol. The localStorage is blocked by default. If you adjust the cookie restrictions it works (don't ask).
13th September: Mission complete!
Before I forget it I submitted my entry shortly before midnight from 12th to 13th September. Luckily I've learned next morning that it got accepted.
I cleaned up my package.json by removing the parts I hadn't found to work on (such as tests ...) and updated the credits.
Then I've read that the version on GitHub should be in a readable format. Which translates to me to JSDoc strings.
During development I saw that nice webpage generated from JSDoc strings in the mastermind-game repo. So I took a peak on what she did to achieve it and learned about the ink-docstrap project.
I found a theme which I like and adjusted my run-script to use it.
Wow. That was a competition!
At least I reached my goal, namely:
- Learning about parcel (not my favourite. Sticking to rollup)
- Learning about Canvas API (avoiding images next time)
- Learning about Game engine (having to learn to think in ticks and not in terms of events or state changes)
- Minifying with terser (a breeze)
- Learning about Audacity (first time I cut audio)
- Learning about indexed images (maybe I can tweak the allowed colours?)
Preparation for next time
For the next time there are some parts I should research upfront respectively do differently:
- How to generate a map?
- How to use WebGL? (I may want to try a 3D game not created with aframe)
- How to get to a working version faster to test it with users?
- How to get text displayed to communicate how to play?
- How to use Canvas API without large PNGs?
- Maybe trying another genre than puzzle games.
- Being more active on Twitter by sharing interim screenshots.
- Adding the start date to my calendar as early as possible.
- Adding true offline functionality (manifest + service worker)
- Generate audio without a file, but in a harmonic fashion.
- Collecting more projects which aim for minimalism and modularity.
- Smaller commits but more often.
- Collection of beautiful sprites / ways to compose elements
- More Wireframes to get an idea of the gameplay earlier.
What have come next?
Some ideas I had but couldn't realise were:
- Showing the elapsed time (or some other kind of score)
- Background music (but I least had sound this year!)
- More prose to better introduce the user into the gameplay
- Link for forking it on GitHub
I hope you learned a bit after reading thus far :-)