Skip to content

How to make a deckbuilder like Slay the Spire

4 min readBy Eduardo Orellana ·

Slay the Spire is a card game where the deck is the character sheet, and every card you take makes every other card rarer. That second idea is arithmetic, so we ran it: a ten-card starter deck in GDScript on Godot 4.7.2, drawn five a turn, and then the same deck with one card added.

Leer en español

The deck is the character, and adding is not free

Mega Crit released Slay the Spire in early access in 2017 and fully in January 2019, and it set the template a generation of deckbuilders followed: fight with a hand of cards, take one new card after each fight, climb a map of branching paths. Monster Train, Inscryption and Balatro all answer it in their own way.

The obvious reading is that a deckbuilder is about collecting good cards. The more useful reading is that it is about the cards you refuse. Every card you add is drawn instead of something else, so a deck gets better by getting more focused, not bigger. Players learn that the hard way, and your design has to make the lesson visible.

Draw, discard, reshuffle: the loop in GDScript

The core is three piles. Draw five cards from the draw pile; at the end of the turn, the hand goes to the discard pile; when the draw pile runs out, the discard pile is shuffled back in. Shuffle with your own RandomNumberGenerator and a seed, not the global one, so a run can be replayed exactly when a player reports a bug.

We ran it with Slay the Spire's opening shape: five Strikes, four Defends and one Bash, ten cards, five drawn a turn, seed 2026. Turn 1 drew Bash, Defend, Strike, Defend, Strike. Turn 2 drew the other five. Then the discard pile was reshuffled, and the pattern repeated: every card appears exactly once every two turns. Bash was in hand on 3 of 6 turns, as it must be.

GDScript
var rng := RandomNumberGenerator.new()
var draw_pile: Array[String] = []
var hand: Array[String] = []
var discard: Array[String] = []

func shuffle(cards: Array[String]) -> void:
	for i in range(cards.size() - 1, 0, -1):
		var j := rng.randi_range(0, i)
		var t := cards[i]
		cards[i] = cards[j]
		cards[j] = t

func draw(n: int) -> void:
	for i in n:
		if draw_pile.is_empty():
			if discard.is_empty():
				return
			draw_pile = discard.duplicate()
			discard.clear()
			shuffle(draw_pile)
		hand.append(draw_pile.pop_back())

func end_turn() -> void:
	discard.append_array(hand)
	hand.clear()

One extra card makes your best card a tenth rarer

Then we added one card, an upgraded Strike, and played 1,000 turns. With eleven cards and five drawn a turn, Bash was in hand on 446 of them, down from one turn in two. Five out of eleven is 45.5 percent; the run landed on 44.6.

That is the whole design tension in one number. The new card is better than a basic Strike, but the card that sets up every big turn now reaches your hand about a tenth less often. This is why Slay the Spire lets you skip a card reward, why removing cards at a shop is a real choice, and why a thin deck can beat a rich one.

Show the player this. A draw pile count and a discard pile count on screen, and the option to look inside both, turn an invisible probability into something a player can reason about.

Energy turns a hand into a decision

Three energy a turn and cards that cost 0, 1 or 2 make each hand a small budget problem: the best cards cannot all be played. Without a cost, the right move is always to play everything, and there is no decision.

The other half is that enemies show what they will do next. Slay the Spire's intents let you plan a turn instead of guessing, so you can choose between blocking and attacking. A deckbuilder without telegraphed enemy moves is a dice game.

The first playable is one fight

Build one fight before one card list. A player with 50 health, one enemy that shows its intent, three energy, the ten-card starter deck and the loop above. Strike deals 6, Defend blocks 5, Bash deals 8 and applies a debuff. When the fight works, add the reward screen: three cards, take one or skip.

Then a second fight that the starter deck cannot win comfortably, so the reward matters. Then the map. A run is a sequence of those decisions, and every system you add should make the next card choice harder, not just the game longer.

Keep cards as data, a Resource per card with its cost, its text and its effect, so adding the fortieth card is an Inspector edit, not new code.

What one person should not attempt

Slay the Spire's depth is hundreds of cards and relics balanced over years of player data. Do not start there. Start with 20 cards that each change how you build, two or three relics, and five enemies with clear intents.

The cards, art and names of Slay the Spire are Mega Crit's. The draw loop above, energy costs and enemy intents are genre conventions; your theme, card list and enemies have to be your own.

2D, and readable at a glance

A deckbuilder is 2D and lives or dies on readable cards: cost, name and effect in one look. In Godot, a card is a Control scene; the hand is an HBoxContainer or a fanned layout you place in code. In the Flockbay app, the card lane battler template is a starting point, and the AI can add the loop above and run it headless, as this post did, while you play the fights and judge them.

A deckbuilder is the cards you refuse as much as the ones you take.

Read more game design notes

Flockbay

The free AI game maker for Windows, macOS and Linux.