Module 10

Card / Deckbuilder Game

Weeks 19-20 | Building and curating a system (your deck) that generates emergent strategy through the random order of card draws.


Prerequisites

ModuleWhat You Used From It
Module 01 - PongBasic game state management, input handling, and game loop fundamentals

Note: This module is the most different from all others in the bootcamp. Every previous module has been real-time — continuous input, frame-by-frame physics, spatial collision. This module is turn-based and UI-heavy. The gameplay loop is: draw cards, evaluate options, play cards, resolve effects, end turn. The hard part is not the game logic — it is making the cards feel good to play through animation and UI.


Week 1: History & Design Theory

The Origin

Magic: The Gathering (Richard Garfield, Wizards of the Coast, 1993) invented the trading card game. Before Magic, games came in a box and every player had the same pieces. Garfield's breakthrough was that the game existed between the matches: you built your deck from a personal collection, choosing which cards to include based on strategy, synergy, and resource constraints. The mana system created a resource economy that forced tradeoffs every turn. A deck was not just a collection of powerful cards — it was a system with its own internal logic, card ratios, and win conditions. Magic proved that the metagame of construction could be as deep as the game itself.

How the Genre Evolved

Hearthstone (Blizzard, 2014) proved that card games could thrive as digital-first experiences. Where Magic was designed for physical cards and adapted to digital, Hearthstone was built for screens from the start. It simplified Magic's complexity — no instant-speed responses during the opponent's turn, automatic mana growth each turn, a maximum of 7 minions on the board — making it accessible to players who would never walk into a game store. Crucially, Hearthstone demonstrated that digital cards could do things physical cards cannot: generate random outcomes, transform into other cards, create cards that never existed in the collection.

Slay the Spire (MegaCrit, 2019) created a new genre by merging deckbuilding with the roguelike structure. Instead of constructing a deck before the game, you built your deck as you played — starting with a weak set of basic cards and adding new ones as rewards after each combat encounter. Slay the Spire's key insight was that smaller decks are often better than larger ones — adding a mediocre card dilutes the probability of drawing your powerful cards. This turned deckbuilding into an editing problem: what you leave out matters as much as what you include.

Balatro (LocalThunk, 2024) exploded the assumption that deckbuilders need fantasy combat. Built around poker hands with modifier cards (Jokers) that transform scoring rules, Balatro proved that the deckbuilder structure works with any card system. Inscryption (Daniel Mullins, 2021) pushed in a different direction, wrapping a deckbuilder inside a horror narrative that broke the fourth wall. Together, these games show that the deckbuilder format is a container — a structure for emergent system-building that can hold any content.

What Makes Deckbuilders Great

A great deckbuilder makes you feel like an engineer. Your deck is a machine you are designing under constraints: limited resources to play cards, limited slots in your hand, limited opportunities to add new cards. The randomness of the draw order means your machine never runs exactly the same way twice, so you must build for resilience rather than a single perfect sequence. The best deckbuilders create moments where cards you added for different reasons accidentally combine into something powerful — emergent behavior from simple rules.

The Essential Mechanic

Building and curating a system (your deck) that generates emergent strategy through the random order of card draws.


Week 2: Build the MVP

What You're Building

A single-player deckbuilder in the style of Slay the Spire: the player fights a series of enemies using a deck of cards, gaining new cards after each victory. Combat is turn-based with an energy system, and enemies telegraph their next action so the player can plan strategically.

Core Concepts (Must Implement)

1. Deck, Hand, and Discard as Data Structures

The core data model of a deckbuilder is three linked collections: the draw pile (a shuffled stack), the hand (an ordered array the player can see and interact with), and the discard pile (a stack of used cards). Drawing a card pops from the draw pile and pushes to the hand. Playing a card removes it from the hand and pushes to the discard pile. When the draw pile is empty, shuffle the discard pile and it becomes the new draw pile.

class DeckState:
    drawPile   = []   // Stack
    hand       = []   // Array
    discardPile = []  // Stack

function shuffle(pile):
    // Fisher-Yates shuffle
    for i from pile.length - 1 down to 1:
        j = randomInt(0, i)
        swap(pile[i], pile[j])

function drawCard(state, count):
    for i in range(count):
        if state.drawPile.isEmpty():
            if state.discardPile.isEmpty():
                return
            state.drawPile = state.discardPile
            state.discardPile = []
            shuffle(state.drawPile)
        card = state.drawPile.pop()
        state.hand.append(card)

function playCard(state, handIndex):
    card = state.hand.removeAt(handIndex)
    state.discardPile.push(card)
    return card

Why it matters: This is a circular buffer system with three stages. The Fisher-Yates shuffle is the only correct O(n) shuffling algorithm. Understanding this cycle is essential because every card you add changes the probability of drawing every other card.

Interactive: Deck / Hand / Discard Visualizer

Click "Draw" to draw cards from the deck to your hand. Click a card in your hand to play it (sends to discard). Click "Shuffle" to return the discard pile to the deck. Counts update live.

2. Card Effect System Using the Command Pattern

Each card is a command object with an execute(gameState) method. A "Strike" card executes dealDamage(enemy, 6). A "Defend" card executes gainBlock(player, 5). A "Draw Two" card executes drawCard(state, 2). This means cards are data, not code branches.

// Card defined as a command object
class Card:
    name        = "Strike"
    cost        = 1
    description = "Deal 6 damage."
    target      = "single_enemy"

    function execute(gameState, target):
        dealDamage(gameState, target, 6)

// Playing a card
function playSelectedCard(gameState, handIndex, target):
    card = gameState.deck.hand[handIndex]
    if gameState.energy >= card.cost:
        gameState.energy -= card.cost
        card.execute(gameState, target)
        gameState.deck.discardPile.push(
            gameState.deck.hand.removeAt(handIndex)
        )

Why it matters: This is the Command pattern. Each card encapsulates an action and its parameters in a self-contained object. Adding a new card means adding a new class, not modifying a switch statement.

Interactive: Card Effect System

You have 3 energy per turn and 4 cards with different effects. Click a card to play it against the enemy. Click "End Turn" to let the enemy act and start a new turn. Defeat the enemy to win!

3. Turn Structure and Phase System

Each turn follows a strict sequence of phases: Draw Phase -> Action Phase -> Enemy Action Phase -> Discard Phase. This is a state machine.

enum TurnPhase:
    DRAW, ACTION, ENEMY_ACTION, DISCARD, CHECK_WIN_LOSS

function advancePhase(gameState):
    switch gameState.phase:
        case DRAW:
            gameState.energy = MAX_ENERGY
            drawCard(gameState.deck, CARDS_PER_DRAW)
            gameState.phase = ACTION

        case ACTION:
            gameState.phase = ENEMY_ACTION

        case ENEMY_ACTION:
            for enemy in gameState.enemies:
                executeIntent(enemy, gameState)
                chooseNextIntent(enemy)
            gameState.phase = DISCARD

        case DISCARD:
            discardHand(gameState.deck)
            gameState.phase = CHECK_WIN_LOSS

        case CHECK_WIN_LOSS:
            if gameState.player.hp <= 0:
                triggerGameOver()
            else if allEnemiesDead(gameState):
                triggerVictory()
            else:
                gameState.phase = DRAW
                advancePhase(gameState)

Why it matters: This is a finite state machine with strict phase ordering. The key insight is that the Action phase is the only phase that waits for external input; all other phases are automatic transitions.

4. Energy/Mana Resource System

The player has a limited amount of energy each turn (typically 3). Each card has a cost. Playing a card deducts its cost. Energy resets at the start of each turn.

gameState.maxEnergy = 3
gameState.energy = gameState.maxEnergy

function canPlayCard(gameState, card):
    return gameState.energy >= card.cost

Why it matters: This is a resource budgeting system. The strategic depth comes from scarcity: if every card cost 0, there would be no decisions. Constraints create gameplay.

5. Enemy Intent / Telegraph System

Enemies declare what they will do next turn, and this information is visible to the player. The player uses this information to decide: do I play defensive cards to absorb the incoming damage, or go all-in on attack?

function chooseNextIntent(enemy):
    roll = random()
    if roll < 0.6:
        enemy.intent = { type: "attack", value: 12, icon: "sword" }
    else if roll < 0.85:
        enemy.intent = { type: "defend", value: 8, icon: "shield" }
    else:
        enemy.intent = { type: "buff", value: 3, effect: "strength", icon: "flame" }

function executeIntent(enemy, gameState):
    switch enemy.intent.type:
        case "attack":
            damage = calculateDamage(enemy.intent.value, enemy, gameState.player)
            applyDamage(gameState.player, damage)
        case "defend":
            enemy.block += enemy.intent.value
        case "buff":
            addStatusEffect(enemy, enemy.intent.effect, enemy.intent.value)

Why it matters: This is information design — deliberately exposing internal state to an external actor so they can make informed decisions. The intent system transforms combat from a guessing game into a planning puzzle.

6. UI-Driven Gameplay

This is the first module where the UI is the gameplay, not just a HUD overlay. The player interacts with cards visually: cards fan out in the hand, hovering over a card shows its details, clicking plays it.

function layoutHand(hand, screenWidth, handY):
    cardWidth = 120
    maxSpread = screenWidth * 0.6
    totalWidth = min(hand.length * cardWidth, maxSpread)
    spacing = totalWidth / max(hand.length - 1, 1)
    startX = (screenWidth - totalWidth) / 2

    for i, card in enumerate(hand):
        card.x = startX + i * spacing
        card.y = handY
        centerIndex = (hand.length - 1) / 2
        card.rotation = (i - centerIndex) * FAN_ANGLE_PER_CARD

Why it matters: In a deckbuilder, the player interacts through UI components — clickable cards, hover states, targeting cursors. State management, event handlers, and z-index stacking are all critical.

7. Status Effects and Modifier Stacking

Status effects like Poison, Strength, Vulnerability, and Block are stateful modifiers attached to an entity. When damage is dealt, the calculation passes through a chain of modifiers: base damage -> modified by Strength -> modified by Vulnerability -> reduced by Block.

// Damage calculation pipeline
function calculateDamage(baseDamage, attacker, target):
    damage = baseDamage

    if "strength" in attacker.statusEffects:
        damage += attacker.statusEffects["strength"].value

    if "vulnerable" in target.statusEffects:
        damage = floor(damage * 1.5)

    return max(damage, 0)

function applyDamage(target, damage):
    if target.block > 0:
        absorbed = min(target.block, damage)
        target.block -= absorbed
        damage -= absorbed
    target.hp -= damage

Why it matters: This is a modifier pipeline. Each modifier is independent and composable. The stacking rules are a state accumulation problem: applying the same effect multiple times must have well-defined behavior.

Interactive: Status Effect Stacking

Add or remove Strength (increases outgoing damage) and Vulnerable (increases incoming damage by 50%). Watch the damage calculation pipeline update in real-time as modifiers stack.

8. Reward and Deck Growth

After winning a combat, the player is offered a choice of 3 new cards to add to their deck (or skip). The critical design insight is that adding a card is not always good. Every card you add dilutes your draw probability for every other card.

function generateRewardCards(playerClass, combatDifficulty):
    cardPool = getCardsForClass(playerClass)
    weights = adjustWeightsByDifficulty(BASE_WEIGHTS, combatDifficulty)

    rewards = []
    for i in range(3):
        rarity = weightedRandomChoice(["common", "uncommon", "rare"], weights)
        card = randomChoice(cardPool.filter(c => c.rarity == rarity))
        rewards.append(card)
    return rewards

Why it matters: Adding a card changes the statistical distribution of every future draw. The "skip" option is the most interesting design element — sometimes the best decision is to not add something.


Stretch Goals

  1. Card upgrades: Allow upgrading a card at rest sites (e.g., "Strike" becomes "Strike+" dealing 9 instead of 6).
  2. Relics / passive items: Persistent items that modify rules globally.
  3. Card removal: Allow removing a card from the deck at a shop or event.
  4. Multiple enemy encounters: Fights with 2-3 enemies simultaneously.

MVP Spec

ElementRequirement
DeckStarting deck of 10 cards with draw pile, hand, and discard pile
Card typesAt least 5 distinct cards: attack, defend, card draw, buff, and debuff
Command patternCards execute effects through a shared interface
Turn structureDraw -> Action -> Enemy Action -> Discard phases as a state machine
Energy3 energy per turn; cards cost 1-3 energy
Enemy intentEnemies display their next action; at least 3 intent types
EnemiesAt least 3 sequential combat encounters
UICards displayed as a hand fan; click to play; hover to inspect
Status effectsAt least 3 status effects with stacking
Damage pipelineDamage modified by attacker buffs, target debuffs, and block
RewardsAfter each combat, choose 1 of 3 cards to add (or skip)
Win/LosePlayer wins by defeating all encounters; loses if HP reaches 0

Deliverable

A playable single-player deckbuilder with at least 3 combat encounters, a working card effect system using the Command pattern, a turn-based phase state machine, an enemy intent system, and post-combat card rewards. Submit the project along with a brief writeup (3-5 sentences) explaining your card effect architecture and how you would extend it to support 50+ unique cards without modifying existing code.


Analogies by Background

These analogies map game dev concepts to patterns you already know. Find your background below.

For Backend Developers

ConceptAnalogy
Deck, Hand, and DiscardMessage processing pipeline — draw pile is the message queue, hand is the processing buffer, discard pile is the completed queue
Card Effect System (Command Pattern)Job queue workers — each job encapsulates its own execution logic; also database migrations with up/down methods
Turn Structure and Phase SystemOrder processing state machine (placed -> paid -> shipped -> delivered) or CI/CD pipeline
Energy/Mana Resource SystemAPI rate limiting — N requests per time window
Enemy Intent / Telegraph SystemHealth check endpoints and observability dashboards — exposing internal state so operators can make informed decisions
UI-Driven GameplayAdmin dashboard or form-heavy CRUD UI
Status Effects and Modifier StackingExpress.js / Django middleware pipeline — each middleware transforms the request/response
Reward and Deck GrowthA/B test variant management — adding a new variant changes probabilities for all others

For Frontend Developers

ConceptAnalogy
Deck, Hand, and DiscardState management with multiple collections — like a Redux store with queue, active, and archive slices
Card Effect System (Command Pattern)React component render props or Vue slots — each card is a self-contained component
Turn Structure and Phase SystemMulti-step wizard or checkout flow
Energy/Mana Resource SystemBudget-constrained UI like a drag-and-drop dashboard with limited widget slots
Enemy Intent / Telegraph SystemTooltip previews and hover states — showing what will happen before the user commits
UI-Driven GameplayThis IS frontend development — CSS transforms, hover states, z-index, and drag-and-drop
Status Effects and Modifier StackingCSS cascade and specificity — multiple style rules stacking on an element
Reward and Deck GrowthFeature toggles in a UI — adding a new toggle increases the combinatorial state space

For Data / ML Engineers

ConceptAnalogy
Deck, Hand, and DiscardSampling without replacement from a finite population
Card Effect System (Command Pattern)Pluggable transform functions in a data pipeline
Turn Structure and Phase SystemDAG-based pipeline orchestration (Airflow, Prefect)
Energy/Mana Resource SystemCompute budget in optimization — limited function evaluations per iteration
Enemy Intent / Telegraph SystemObservable state in a POMDP — partial observability
UI-Driven GameplayInteractive data visualization dashboards (Plotly, Streamlit)
Status Effects and Modifier StackingFeature transformation pipeline — base value passes through a chain of vectorized operations
Reward and Deck GrowthAdding features to a model — each new card changes the probability distribution of draws

Discussion Questions

  1. The Command pattern makes cards data-driven. How would you implement a card that says "Deal damage equal to the number of cards in your discard pile"? What about "Copy the last card you played and play it again"? Where does the Command pattern break down?
  2. Smaller decks are often better than larger decks. This is counterintuitive — more options should be better, right? How does this map to software architecture? When has adding a feature, dependency, or service made your system worse overall?
  3. The damage calculation pipeline passes through multiple modifiers. What happens when you need to add a new modifier that interacts with existing ones? How do you manage ordering and priority?
  4. Enemy intent gives perfect information about the next turn but uncertainty about the turn after. How does this balance of known and unknown information create strategic depth?