Building a Balatro-lite game with vanilla web tech

I’ve spent countless hours playing Balatro, and I’ve always wanted to create my own game with similar mechanics. When TrinketOS announced a game jam, it was the perfect excuse to make it happen.

TrinketOS is an Android launcher with built-in lore and a meta-game:

At its core, TrinketOS is a modular interface altar:

  • A launcher that respects buttons and joysticks.
  • A customizable home that reads like a game and plays like a tool.
  • A system that rewards you with hats and achievements for exploring its circuits.

The game jam required building a “Tapp” - an HTML/JS/CSS app that runs inside TrinketOS’s webview. There’s a great overview on Github.

I called it Stellante - a zodiac-themed Balatro-lite. Poker hands to progress through antes, ships (jokers) that modify scoring, boss blinds based on zodiac signs reimagined as spacecraft. The name comes from “Stellar” (space) and “Ante” (the poker term for progression).

You can play it at omgmog.net/stellante.

Building the foundation

I didn’t have a clear vision of where this would end up. I just knew I wanted to capture Balatro’s feel - that rough, shaky pixel aesthetic, the satisfying card animations, the way scoring builds up. I’d worked on web-based casino games before, so at least I knew my way around card rendering and deck systems.

I made a list of the core features the game would need: card renderer, deck system, animation queue, poker hand evaluation, joker modifiers, shop phase, progression system, save/load. Then I started building them one by one. First the renderer - drawing random hands of cards on click. Then a proper deck to draw from. Animation queueing for card movements. Basic game loop with menu, play, and game over phases.

Next came scoring for poker hands (pairs, straights, flushes, etc.) and the Balatro-inspired bits: jokers with score modifying effects, a buy phase to purchase them between rounds, and a save/load system using localStorage. The save system also handles basic preferences for sound and music.

Space zodiac theming

Instead of copying Balatro’s aesthetic directly, I wanted something different. I settled on a zodiac theme - reimagining the twelve zodiac signs as spacecraft types. Aries became a corvette (fast, assault-focused), Taurus a cruiser (industrial, scaling power), Leo a battlecruiser (commanding, demanding uniqueness).

I replaced the traditional card suits with elemental symbols. Fire, Earth, Water, and Air instead of hearts, diamonds, clubs, and spades. Abstract geometric shapes that matched the space setting. The faction system used this - ships came in elemental variants, and the suits on the cards mattered for scoring.

I used ChatGPT to craft prompts, fed them into PixelLab for image generation, used Python for palette alignment, then back to PixelLab’s image-to-image generation for elemental variations. Tweaked prompts as I went, kept them consistent - same style, similar detail level, cohesive pixel-crunch aesthetic across all 12 zodiac ships and their variants.

I put face cards through the same process, with a final step in Photoshop to create the traditional mirrored/split layout for Jacks, Queens, and Kings. Backgrounds matched the elemental joker backgrounds.

I grabbed audio from itch.io - an 8-bit platformer music kit and various sound effect packs.

The ships ended up being the most important part. 12 zodiac types, each with five versions: a prototype plus four elemental variants (Fire, Earth, Air, Water). That’s 60 ships total. Each zodiac has its own scoring mechanic, and the elemental variants each have different effects from their prototype.

I built a set bonus system that rewards you for collecting either all four ships of the same element (faction sets like “Fire Legion”) or all four elemental variants of one zodiac sign (line sets like “Aries Line”).

This changed the feel completely. Instead of playing cards, it felt like commanding a fleet. Instead of collecting jokers, I was acquiring ships with special abilities. Playing cards with flame symbols whilst commanding fire-element ships made more sense than traditional hearts and spades in a space setting.

Shop mechanics

Between each blind, there’s a buy phase to buy ships. Runs start with 20 chips, earning more by beating blinds.

The shop offers a limited selection each round - just two ships in the first two antes, then three afterwards. In ante 1, only prototype ships appear. From ante 2 onwards, all 60 ships become available. The shop won’t offer duplicates of ships already owned, and there’s a limit of five ships total.

I added faction discounts. Base cost for most ships is 12 chips, but owning ships of the same element makes matching variants cheaper - up to 2 chips off per owned ship of that faction. Going all-in on one element pays off, but narrows your options.

Unwanted ships sell for half their base cost. Rerolling the entire shop costs 5 chips and gives three new options. More to juggle.

I wanted tension between short-term needs and long-term strategy. Early discounts push towards mono-element builds, but that can lock out zodiac abilities needed for later boss blinds.

Progression system

I moved from continuous rounds to Balatro’s ante and blind system. Each ante has three blinds with escalating score requirements.

The third blind in each ante is a boss blind with special modifiers, for example:

  • Aries: forces exactly 5 cards to be played
  • Gemini: deals some cards face down
  • Leo: makes royal cards (J/Q/K) score zero
  • Libra: treats duplicate ranks as singles
  • Pisces: only counts the lowest 5 ranks
  • … and so on for the rest of the zodiacs

Getting these effects working was fiddly. Boss blinds don’t just change scoring - they modify which cards can participate in hands.

Boss blinds reuse the prototype/base ship sprite for each zodiac, with intro/outro animations, and feedback during the blind.

Tech stack

Building as a Tapp meant working within TrinketOS’s webview constraints. No native game engines, no heavy frameworks - just HTML/JS/CSS that could run efficiently inside the webview on Android handhelds with varying performance.

I built the game in TypeScript - separate systems for state, input, rendering, animations, and game logic. Canvas 2D for cards and UI. WebGL shader for the cosmic background, with a fallback to a simple static background for devices that don’t support it.

I used Vite to handle local development and bundling. I used dirty rectangles for optimised redraws, pooled animations to cut down on garbage collection, and a frame rate manager to keep things smooth.

Shipping it

I finished it enough to submit to the game jam - the core loop worked, boss blinds fired properly, ships modified scoring the way they should. Final build came to 7.4MB.

Winners were decided by community votes on the TrinketOS Matrix. The creator of TrinketOS reviewed all the entries in this video:

I didn’t win, and I haven’t touched it since the game jam ended. Mobile controls could be better, and the balance needs work. But it runs, the core loop works, and building a full game with vanilla web tech was exactly what I wanted to try.

The zodiac theme gave it enough personality to stand out from other Balatro-likes. I got to mess around with AI image generation workflows, proper state management in TypeScript, and all the fiddly bits of card game logic.

You can play it in the browser at omgmog.net/stellante.

This post is also available in plain text

[Comments]

Want to comment? You can do so via Github.
Comments via Github are currently closed.

[Webmentions]

Want to reply? I've hooked up Webmentions, so give it a go!