Edge Not Found postmortem — js13k 2020

September 22, 2020

Title screen of Edge Not Found.

So I par­tic­i­pat­ed in js13k again this year, this time stick­ing to my cards bet­ter and decid­ed to make an eye- as much as a brain-bend­ing puz­zle game called Edge Not Found. After work­ing for almost a year in Type­Script (and Can­vas) for my job, I feel I have a lot more expe­ri­ence work­ing with the brows­er, along with the things I’ve learned from my entry last year. So here’s anoth­er larg­er-than-13kb post­mortem about a small­en-than-13kb game.

js13k last year was the most fun I’ve had with a game jam. There’s a lot of activ­i­ty from devs on Twit­ter (though not over­whelm­ing­ly so), the month-long dead­line is very relaxed com­pared to some oth­er game jams (you’ll fin­ish on time even if you con­tribute just half a kilo­byte per day!), and there are sim­ply some amaz­ing tech­ni­cal mar­vels that peo­ple cre­ate even despite all these lim­i­ta­tions.

Last year I want­ed to try out the Zdog library, this time my choice fell on rough.js, which cre­ates seem­ing­ly hand-drawn ver­sions of geo­met­ri­cal shapes like cubes and cir­cles. I stum­bled upon a blog post that explained how this effect was achieved, which made me want to try it out. Next to that, I’ve been work­ing on a mini JS frame­work called Toma­to that sup­port­ed tim­ing, hit­box and input man­age­ment, which I want­ed (and did) improve dur­ing the jam. And pro­gram­ming in JavaScript with a live reload plug-in is absolute­ly a blast thanks to the blaz­ing­ly fast iter­a­tion time.

And, of course, a seed for a game idea. I’ve made a cou­ple Sokoban vari­ants before, and my next idea was to have the lev­el con­tin­ue infi­nite­ly into all direc­tions. A small spoil­er warn­ing, though: I’ll vague­ly describe some of the puz­zle mechan­ics (though no puz­zle solu­tions!) in this post­mortem, so you might want to play the game a bit before read­ing on.

Programming

A lev­el in this game is defined with a 2D array to set up the grid and some meta­da­ta such as the lev­el name, but also shift off­sets that are used for lat­er lev­els. I think undo is the most impor­tant func­tion­al­i­ty any puz­zle game should sup­port, and as such I built the game up in a way where the game & ren­der­ing log­ic are sep­a­rat­ed to make that pos­si­ble.

Definition of the level in code, next to the level as it displays in the game.

The rec­tan­gu­lar bor­der in the mid­dle of the screen indi­cates the only part of the lev­el that actu­al­ly exists. If you walk through the bor­der, as far as the lev­el state is con­cerned, you’ve wrapped to the oth­er side of the lev­el. There’s some math to decide where you should end up if the lev­el is “shift­ed” (the lev­el grid is not uni­form­ly aligned).

One thing I attempt­ed to imple­ment was pathfind­ing. Late­ly, a num­ber of Sokoban-style puz­zle games have popped up that allow play­ers to click or tap a loca­tion and have the char­ac­ter fig­ure out how to walk over there. I real­ly want­ed to have this fea­ture, but A* pathfind­ing was not intend­ed for infi­nite grids. I real­ly want to imple­ment this for my next puz­zle game, though.

Full source code of the submitted version of Edge Not Found.

Optimization

Make no mis­take: even code that fits on a flop­py disk can be incred­i­bly slow when draw­ing a lot of things to the screen. At first I drew a rough.js prim­i­tive for every block on the screen, which is where this lim­i­ta­tion became evi­dent. Then, I drew the shapes to a lev­el can­vas, which is then drawn as an image and repeat­ed across the screen. This was bet­ter, but still slow on lev­els with a lot of objects. So then I also drew all indi­vid­ual objects to their own can­vas­es, drew those onto the lev­el, then drew that repeat­ed­ly on the main can­vas. Also, the game now attempts to skip ren­der­ing when noth­ing changes, using the object-lev­el-main can­vas tech­nique described above, and that caused a more sig­nif­i­cant improve­ment than I orig­i­nal­ly expect­ed.

A nice ben­e­fit is that all of this makes it real­ly easy to sup­port mul­ti­ple col­or palettes, too. I made a cou­ple of nice ones, includ­ing one based on Oki­na­ki (my favorite col­or theme in N++) and Back­Flipped, a theme with the col­or palette from my pre­vi­ous entry.

Screenshot of the game with the Ikaniko color palette.

I made a lin­ter for my entry last year, but I expand­ed it for this game, and most­ly re-pur­posed it as a way to put mul­ti­ple JavaScript files in a sin­gle file to mini­fy it else­where, Google Clo­sure Com­pil­er in my case. Last year I had a lot of trou­ble with the Clo­sure Com­pil­er, but because of my choic­es of libraries com­bined with a bet­ter cod­ing style, it worked with min­i­mal hic­cups this time around, allow­ing for a bit more con­tent. Ulti­mate­ly, I was able to mini­fy around 79kb of files to just below 13kb zipped.

Level design

Mak­ing puz­zles for this game took a lot of iter­a­tion, but over­all I’m real­ly hap­py with the lev­el of puz­zle qual­i­ty in Edge Not Found. I iter­at­ed a lot and kept the very best lev­els, and made sure to enforce the intend­ed solu­tion. In world 1 and 2 you’re still get­ting used to the wrap­ping world and the puz­zles are easy: the wrapped grid makes sure that the pos­si­bil­i­ty space is big­ger than in an ordi­nary ver­sion of Sokoban because it’s eas­i­er to retrieve blocks from tight cor­ners in this game. But every­thing from 300 onward show­cas­es very inter­est­ing prop­er­ties of the sys­tem that are real­ly fun and mind-bend­ing to dis­cov­er, and in some cas­es allow for very hard puz­zles. I am still a bit divid­ed on the new mechan­ic intro­duced in the final world, which I would prefer­ably have intro­duced ear­li­er, but since it most­ly exists to com­pli­ment the oth­er mechan­ics in the game, it does make for a fit­ting finale of the game togeth­er with lev­el 404.

Level 301 of Edge Not Found.

I espe­cial­ly love how the lev­el select came togeth­er. Ini­tial­ly, I was plan­ning for the [Esc] menu to be used for nav­i­gate between lev­els, but pro­cras­ti­na­tion hit me hard. I end­ed up imple­ment­ing it as a reg­u­lar lev­el instead, allow­ing me more con­trol over in which order the play­er could play the lev­els.

Level Select of Edge Not Found.

Ini­tial­ly, all lev­els could be accessed at the start of the game. I decid­ed to gate it slight­ly, to make for a smoother dif­fi­cul­ty curve and a bet­ter sense of pro­gres­sion. You don’t have to beat all of the lev­els to con­tin­ue, since some of them are real­ly hard and I want play­ers to have a real­is­tic chance to unlock lev­el 404 and fin­ish the game. The main prob­lem that spawned out of this is that the lev­el select would become so big, it would no longer fit on the screen. That’s how I end­ed up with the cur­rent tight design, which already teas­es the mechan­ics of lev­el 200 a lit­tle bit if you pay atten­tion.

Conclusion

I had a blast mak­ing this game, and the recep­tion also seems a lot bet­ter than for my entry last year. It was a great test run for my mini frame­work Toma­to, and I worked out a lot of the kinks for the first game using it. I’m cer­tain­ly look­ing for­ward to doing more projects like Edge Not Found, and I’m real­ly curi­ous to how it will do in the js13k vot­ing.

If you have some time and are now real­ly excit­ed about play­ing the game, you can play either the con­test sub­mis­sion or the itch ver­sion! Thank you for read­ing.


  or subscribe here!