Cat Survivors: How We Squeezed a Whole Game into 13KB for js13k 2025

cat survivors game

Hey everyone! Every year, the js13k competition challenges developers to create a game based on a specific theme in just one month, with the final ZIP archive being no larger than 13,312 bytes. The theme for 2025 was Black Cat. We eagerly accepted the challenge with a few goals in mind: work smart, not hard; achieve maximum gameplay with minimum overhead; and, most importantly, focus on the game itself!

Table of Contents


The Outcome: Key Takeaways and Lessons Learned

The game was completed and submitted on time. We're happy with the result: an addictive gameplay loop with leveling, weather changes, bosses, and even a proper ending. Here are the main lessons we learned:


The Creation Process: From Idea to Release

The Idea and the Cat-Base

The idea came instantly: an auto-shooter in the style of Vampire Survivors, but starring a charming black cat.

vampire survivors game

To stand out, we ditched pixel art in favor of vector graphics. We started with procedural animation for the cat, which took a bit of time but immediately gave the game a polished and smooth look, similar to mobile hits.

black cat sketch

💪 I think everything turned out so well only because we had a real-life prototype — our beloved black cat, Chichi. She personally supervised every stage of development!

Next, we added the basic gameplay: the first enemy — the Fly (chosen by a vote!) — a start screen, and a game-over screen. At that point, we still weren't sure if the game would have an ending.

Ramping Up the Content

From there, the game started growing with content: passive abilities, weapons, power-ups, new enemies, and, of course, balancing the progression. We added screens for selecting upgrades and opening chests. With each new element, the game felt more complete.

⚠️ The Fear of Bloat. The original Vampire Survivors has weapon evolutions — a powerful upgrade combining a weapon and a passive item. I regretfully disabled this mechanic, fearing it would push the game over the 13KB limit. Every new weapon, description, and unique firing system linearly increased the archive size.

gameover screen

We added some visual customization: certain abilities change the cat's appearance, giving it glasses or lips. We had plans for more of these cosmetic changes so the player's build would be reflected in their look.

The upgrade system isn't perfect either. Initially, the game forces you to choose three new weapons, and then it offers random upgrades. In the future, we want to refine this: for instance, by offering an upgrade for an existing weapon alongside two new ones.

Is There an Ending? Yes!

We're thrilled that we managed to give the game a clear goal and a romantic ending. In the original Vampire Survivors, you can't defeat death, which, honestly, is a bit of a letdown. When a player realizes there's no finale, it can be disappointing. In our game, you get a chance to win!

I'm also glad we handled the scenario where all upgrades are maxed out. In this case, the game automatically starts leveling up the character's base stats. This was an easy detail to overlook.

To make the gameplay more dramatic, we added changing weather, which visually connects the monsters and the environment.

The final touch was tuning the enemy waves and balancing the difficulty. We intentionally designed it so that even if a player loses a few times, they can quickly get the hang of it and eventually enjoy feeling powerful during a 10-minute session.

Optimizing Stat Calculations

Initially, stats were calculated using a classic formula: value = base * (1 + mod) + add. Upgrades provided percentage-based boosts, which required storing redundant data and complicated the calculations. In the UI, this appeared as a vague "+20%."

I decided to simplify everything. In the new system, weapon and character stats are simply summed up. This allowed us to show the player the exact increase in weapon parameters and, best of all, it reduced the archive size by 100 bytes! An unexpected and impressive result.

stat effect ui

Collision Detection

The game features many enemies that push each other around. We deliberately chose the simplest and slowest algorithm — a brute-force "every-to-every" check:

for (let i = 0; i < N; ++i) {
  for (let j = i + 1; j < N; ++j) {
    // check enemy[i] vs enemy[j]
  }
}

💡 Why doesn't it lag? Typically, spatial partitioning (like a grid) is used for such tasks to check for collisions only within a specific area. But to save space, I skipped it. I expected serious performance issues, but modern devices handled it just fine. As a backup, we could have limited the number of enemies through game design or disabled collisions for those off-screen.

To offload the system, I put flying enemies (flies) on a separate layer — they simply fly over the others.


Knowledge Base: What's Inside the Game?

Weapons (11 total):

Passive Abilities (12 total):

Enemies (5 archetypes):

We also had plans for Pigeons and Raccoons.

Champions and bosses drop a Can of Food, which grants 1 or even 3 upgrades. And from a surprise Box, you can get a cross (kills all enemies), a magnet, health, or experience.


The Code: Aggressive Minification

zip size history

Bytes were our main enemy. We used aggressive minification techniques, sometimes at the expense of readability and performance. But for js13k, it's a worthy trade-off.

Banned Keywords

I forbade myself from using certain JavaScript keywords to save space:

💡 Local Variable Trick. To get rid of {} and return, you can declare local variables directly in the arrow function's arguments.

// Before
const fn = (x, y) => {
  let value = calc(x, y);
  return s(value);
};

// After
const fn = (x, y, value = calc(x, y)) => s(value);

Math

The ** operator is a great replacement for Math.sqrt(x) with x**.5 and Math.pow(x, y) with x**y. I re-export all necessary functions from Math so they can be minified. For example, export const {sin, cos, atan2, exp, hypot, random, ...} = Math;.

And to calculate the length of a vector, I use hypot(x, y) instead of sqrt(x*x + y*y).


The Build: From TypeScript to ZIP

Our build pipeline looked like this:

  1. Rollup + TypeScript Transformers: Compile TypeScript with custom transformations (e.g., for renaming fields).
  2. esbuild: The first minification pass.
  3. Terser: More aggressive minification.
  4. Roadroller: Pack HTML/CSS into JS and create the final HTML file.
  5. advzip: Compress into a ZIP archive.
  6. Efficient Compression Tool: Further compress the ZIP file.

🚀 Future Idea: Write a Class Crasher — a transformer that automatically converts simple classes into interfaces and a set of functions. This could yield even more savings.


Graphics and Controls

Canvas, Not WebGL

I'm a big fan of WebGL, but this time I decided to use a pure Canvas context, taking devicePixelRatio into account. I was worried the game would lag on large screens, but modern devices handled it like a champ. It was a pleasant surprise and saved a lot of space.

Canvas Tips:

Controls

Pointer Events handled mouse and touch events perfectly. The code was very minimal. For the keyboard, I used KeyboardEvent.code and this little trick to determine direction:

const x =
  (isDown["KeyD"] | isDown["ArrowRight"]) -
  (isDown["KeyA"] | isDown["ArrowLeft"]);
const y =
  (isDown["KeyS"] | isDown["ArrowDown"]) - (isDown["KeyW"] | isDown["ArrowUp"]);

The bitwise OR (|) is used here so that undefined | 1 becomes 1 instead of NaN.

😩 My Biggest Mistake: I didn't add gamepad support from the beginning. When I finally got around to it at the end, I realized it required rewriting all the upgrade selection screens. There just wasn't enough time.


Audio: ZZFX and Procedural Music

For me, sound isn't an option; it's half the game's atmosphere. 🎶 Some people cut audio to fit the size limit, but this year the rules got stricter: no sound, no score in that category. That's why I always find a way to add sound to my games, even if it doesn't sound professional.

For sound effects, I used ZZFX — it's probably the easiest and fastest way to add audio to a game. I tried generating music with chatgpt and deepseek, but the results were awful. In the end, I settled for a simple drum machine, wrote a melody generator based on random patterns in A-major, and added a basic arrangement. It turned out a bit repetitive, but it's better than nothing.

💡 Idea for the Future: Use samples generated in ZZFX as instruments for procedural music. This could save a few more bytes.


Conclusion

Here we are, celebrating the end of the competition with all the participants and organizers! This was our fourth js13k game, and it gets more interesting every year. The contest forces you to rethink your tools, optimization strategies, and web standards. A big thanks to all the participants for this event, and a special shoutout to the organizer, Andrzej Mazur!

We plan to continue developing the game: we'll add new characters, progression, locations, and achievements. Follow me on social media to stay updated!

What do you think? Have you faced similar challenges? What are your favorite minification tricks? Share your thoughts in my Telegram channel or send me an email!


Our Previous js13k Entries

← Prev.
🍄 The Last of Us: Playing the Game After the Series — First Impressions