Build a Balatro-Style Deckbuilder in Phaser


To build a Balatro-style deckbuilder in Phaser, you need three systems: a card deck you can deal from, a poker-hand scorer that produces Chips x Mult, and a blind target that rises each round. This tutorial walks through all three, and the playable prototype below shows the finished result running in your browser right now.

How This Guide Was Built

A quick honesty note before we start. This guide is based on official Phaser documentation, published design references like the Wikipedia page on Balatro, and the working prototype shipped alongside this post. No hands-on playtesting of the commercial game was performed, and the publisher’s sales announcements are cited only as context. Everything here is desk research plus original code.

Controls: Click cards to select them, SPACE to play the hand, D to discard, R to restart.

What makes Balatro’s scoring loop work?

Balatro’s loop works because it layers three simple numbers: Chips, Mult, and a rising blind target. You play poker hands to score Chips multiplied by Mult, Jokers bend the math in your favor, and failing a blind ends the run. The 2024 poker roguelike by solo developer LocalThunk, published by Playstack, passed 5 million copies sold by January 2025.

How do you represent a deck of cards in Phaser 3?

Represent a deck as an array of plain objects, each holding a rank and a suit. Build the 52-card array by looping suits and ranks, then shuffle with a simple Fisher-Yates pass before dealing. In Phaser you need no image assets at all: the prototype draws every card as a colored rectangle with text on top.

function buildDeck() {
  const suits = ['♠', '♥', '♦', '♣'];
  const ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
  const deck = [];
  for (const s of suits)
    for (const r of ranks)
      deck.push({ rank: r, suit: s });
  return deck;
}

Dealing the 8-card hand is just splicing from the shuffled deck. Rendering each card uses Phaser’s game object factory:

this.add.rectangle(x, y, 70, 100, 0x2a2440)
  .setStrokeStyle(2, 0xa855f7)
  .setInteractive();
this.add.text(x, y, `${rank}${suit}`, {
  fontFamily: 'monospace', fontSize: '20px', color: '#e9d5ff'
}).setOrigin(0.5);

How do you score poker hands in JavaScript?

Score poker hands by detecting the hand type first, then applying that hand’s base Chips and Mult, then adding bonuses for each scoring card. It recognizes the standard set, High Card up to Royal Flush, with simple counters: sort ranks, count duplicates, and check flush and straight conditions. Because a hand is five cards, these checks stay small.

function detectHand(cards) {
  const counts = {};
  cards.forEach(c => counts[c.rank] = (counts[c.rank] || 0) + 1);
  const pairs = Object.values(counts).filter(n => n > 1);
  if (pairs.includes(4)) return 'Four of a Kind';
  if (pairs.includes(3) && pairs.includes(2)) return 'Full House';
  if (pairs.includes(3)) return 'Three of a Kind';
  if (pairs.length === 2) return 'Two Pair';
  if (pairs.length === 1) return 'One Pair';
  return 'High Card';
}

The scoring formula itself is one line at heart:

const score = chips * mult;
this.add.text(cx, cy, `${chips} x ${mult} = ${score}`, {
  fontSize: '28px', color: '#f0abfc'
}).setOrigin(0.5);

Discards work the same way as plays: three discards per blind, each removing selected cards and redrawing from the deck.

How do you add Jokers and a shop between blinds?

Add Jokers as data objects with a description and a score-modifying function, then draft a few between blinds. When a hand scores, loop through your held Jokers and let each one adjust chips, mult, or both. The prototype offers a Joker draft between its three escalating blinds, enough to make runs feel different without a full shop economy.

const JOKERS = [
  { name: 'Purple Joker', desc: '+4 Mult flat',
    apply: s => { s.mult += 4; } },
  { name: 'Even Steven', desc: '+2 Mult per even card',
    apply: (s, hand) => { s.mult += hand
      .filter(c => parseInt(c.rank) % 2 === 0).length * 2; } }
];

Applying them is a single loop before the final score is compared against the blind target.

How do you keep a single-file Phaser game maintainable?

Keep a single-file Phaser game maintainable by separating data, logic, and rendering into clearly labeled sections, and by using one scene with named functions instead of tangled inline callbacks. The prototype is one self-contained HTML file with zero image assets, so structure matters more than usual. Phaser’s scale manager handles responsive sizing cleanly:

new Phaser.Game({
  type: Phaser.AUTO,
  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    width: 800, height: 600
  },
  scene: [TableScene]
});

Inside the scene, keep your state in a few named fields — deck, hand, selected, blind, discards — and reset them in one startBlind() method. If a function touches the DOM or the canvas directly, ask whether it belongs in the data section instead.

FAQ

Do I need art skills or image assets to follow along?

No. Every visual in the prototype is a Phaser rectangle or text object, generated procedurally in code. You can ship a complete, playable deckbuilder with nothing but colors, shapes, and a monospace font, then swap in real art later if you want.

How close is this prototype to the real Balatro?

It borrows the scoring loop — poker hands, Chips x Mult, rising blinds, Jokers — but it is an original, much smaller game. The commercial release has hundreds of Jokers, extra decks, and boss blinds; the Wikipedia summary covers that scope well. Think of this as the skeleton, not the clone.

What should I build next after finishing this tutorial?

Good next steps: add more Jokers with conditional triggers, introduce money and a real shop with prices, or persist a best-score with localStorage. Each one teaches a different skill — event-driven effects, economy balancing, and state persistence — and each slots into the structure you already have.

What You Learned

  • How Balatro’s Chips x Mult scoring loop creates tension against rising blind targets, and why it has resonated with millions of players
  • How to build and shuffle a 52-card deck as plain JavaScript data and render it procedurally in Phaser 3 with no image assets
  • How to detect poker hands from High Card to Royal Flush with small, readable counting logic
  • How to layer Jokers on top of scoring as data-driven modifier functions
  • How to structure a single-file Phaser game so it stays maintainable as you add features