A picross game in 1024 bytes

by
, posted

I love picross puzzles. They’re fun little logic games, like a mix between Sudoku and a crossword. Given a blank canvas, you’re given hints about which squares to fill in, and you use logic to fill in a picture. (If you’re looking to get into picross, I recommend this article/video.)

For this year’s JS1024, I built a simple picross game that fits in just one kilobyte of HTML. I was very proud to win third place!

It was challenging to fit this all in just 1024 bytes! This post is a retrospective on the project.

Mistake #1: random chess

This year’s JS1024 theme was “luck”.

My first idea was to add luck to a game that doesn’t normally have it. After I found out that Unicode has chess symbols, I decided to make a chess game where your pieces are random.

Screenshot of a work-in-progress chess game that I never finished

I built a basic data model and “renderer” (if you can even call it that). But when I started implementing the basic movement rules for a single piece—the rook—I began to realize that this wasn’t going to work. I felt that the rules of chess were too complex for me to fit in a kilobyte, let alone a primitive AI.

Later, I learned that this was done for JS1k 2010 (13 years ago!). So it’s possible…but its creator seems to be an expert at creating small chess programs, which I am not.

After some brainstorming, I decided to scrap everything and make a picross game.

Mistake #2: trying to beat RegPack

I’ve used RegPack in all of my JS1k and JS1024 submissions. Frankly, I don’t understand how it works, but it sure crushes JavaScript source code better than anything else I’ve used.

This year, I tried JavaScript’s new-ish compression APIs. I figured I could compress the source code during build time (using Zopfli for a big reduction), then decompress and eval() it in my submission.

Maybe I could do better than RegPack!

The compression code was a little verbose, but it basically ran my (minified) source code through Zopfli and converted it to a string. For example, alert("Hello") becomes the horrible string "KÌI-*ÑPòHÍÉÉWÒ\x04\x00".

From there, I could decompress and eval() the code at runtime. This is what I came up with:

let decompressionStream = new DecompressionStream("deflate");

let deflatedBytes = [...deflatedString].map((a) => a.codePointAt(0));

new Blob([new Uint8Array(deflatedBytes)])
  .stream()
  .pipeThrough(decompressionStream);

let bytes = [];
for await (let chunk of decompressionStream.readable) {
  bytes.push(...chunk);
}
let source = String.fromCharCode(...bytes);

eval(source);

I compressed this a bit more but couldn’t make this solution work better than RegPack.

I’d like to explore this idea further. It might be useful for embedding larger resources, like images, into JavaScript source code. But for this submission, I gave up on using the compression APIs.

New for me this year: Deno

I’ve been enjoying Deno lately. Compared to Node, I love its built-in support for TypeScript, formatting, linting, testing, and sandboxing. It’s not perfect, but it’s so good that I don’t have Node installed on my personal machines. (I’m not completely out of the Node game—I just use it in a VM now.)

This year, I replaced three Node-based tools with Deno-based ones:

  1. Linting, where I replaced ESLint with deno lint
  2. Formatting, where I replaced Prettier with deno fmt
  3. Build scripts, which had to be completely rewritten

I was happy with this, but I’ve preferred Deno for awhile and this project was nothing new. I don’t like that I’m doing free marketing for a company right now, but their open-source offering is great and I hope it continues to be maintained.

I also used Deno for another new thing this year: type checking.

New for me this year: type checking

I like TypeScript because I generally like type checking, especially in JavaScript with its tricky type system. However, it didn’t seem appropriate for a code golf challenge like this. There were certainly going to be times where I needed to do cursed things: global variables, eval(), using 0 and 1 for booleans, and so on.

In other words, I wanted to write my solution in JavaScript, where I didn’t have type checking.

But there was good news for me: TypeScript can check JavaScript files.

I declared various types in types.d.ts:

// This is an abridged version of my code.

declare global {
  /**
   * Get the hints for a row.
   */
  let z: (row: boolean[]) => number[];

  // ...more types...
}

// This type is used so we can import something
// to pick up the global declarations.
export type DummyType = never;

Then, in my submission file, I made sure to import these types and declare that the file should be type checked.

// @ts-check

/** @typedef {import("./types.d.ts").DummyType} DummyType */

// Use the global type defined in `types.d.ts`.
z = (row) => {
  // ...
};

// ...

This helped me a bunch! It’s hard enough to keep types straight when you’re working on a well-structured codebase, but I did a lot of strange stuff in this project. Type information was helpful when most of my variables were one character. It also helped refactoring which I did a few times.

Next year, I might try writing the whole thing in TypeScript, and be ready to use any and // @ts-ignore when I need to.

First get it working…

In past years, I’ve done code golfing while I work. For example, if I’m making a new variable, I’ll choose a short variable name. It’s usually harder to read this code, which made my life more difficult.

I tried to be a little more intentional this year. I wanted to build something reasonably functional with few minification tricks. Roughly speaking, I tried to get it about 75% feature complete before I started really working on compression. I was keeping an eye on the size as I worked, but not closely.

I pretty quickly got a working version of the game using fairly readable code. It had functions like getHorizontalHints() and initialRender(). It had variables like state and board. The code was spartan and I’m sure others wouldn’t find the code as readable, but it worked.

Unfortunately, this “nice” version was 384 bytes too big.

…then make it small

With 384 bytes to remove, I started chopping away at my solution.

First, I did the easy task of shortening everything. Descriptive functions like getWidth() got mercilessly renamed to w. state.puzzleIndex became global variable m. All the while, I was adding these to my type definitions, clawing back some of the benefits of a descriptive name. Doing this was fairly easy but saved 214 bytes—more than half of what I needed to destroy!

The remaining 170 bytes were harder to remove and required many solutions. I saved 65 bytes by cleaning up my loops. Assuming that puzzles were squares let me save 10 bytes in a small refactor. Shortening the way I defined puzzles saved another 34, but removed a helpful feature. 5 more bytes disappeared after tweaking my RegPack settings. Refactors that removed code totaled to another 28.

After a bunch of paring down, I had a “surplus”: I was down to 1009 bytes. 15 whole bytes to spare! I added back one of the features I removed, but had to make room for it by making some sacrifices to the UI—I think it was worth it.

As you might imagine, I’ve simplified this story a bit. This post weaves a clearer narrative than my messy commit history. But even with the bumps, I liked this approach and would do it again. Not just for code golf challenges; I like the “get something working, then iterate” workflow.

Conclusion

I always enjoy JS1024. It’s fun to try to make something with such tight constraints, and a nice reminder that JavaScript doesn’t have to be bloated. It’s also helpful practice, especially with the new tools I used.

If you want, you can check out my submission on the JS1024 site or the source code.

Until next year!