How to make a card game with AI
A card game is a shuffle and a state machine. The one line shuffle is biased, and nested conditions are what make a new rule impossible to add later.
Jump to a section
Two small things decide whether a card game holds up, and a first build usually gets both of them wrong. The shuffle has to be even, which the one line version is not. The turn order has to be a state machine, because that is what lets you add a rule next week without unpicking the rest.
A biased shuffle still deals cards and nobody notices. A turn order written as nested conditions still plays a hand through. Neither shows up in a first test. Both show up the moment you want trumps, a second round, or an opponent allowed to pass.
Four steps to a hand that plays through#
Four lines, and the one part of a card game you should never improvise. Get it in before anything can be dealt.
Write the list out. If you cannot name the phases, the game has intentions rather than rules.
Cards snapping into place is fine for now. Once things slide, a rule bug and a timing bug look the same.
A queue of card slides sits on top of a game that already works correctly without any of it.
Build a trick taking card game as ONE self-contained .html file. No
frameworks and no image files. A card is a rounded rectangle with a suit
symbol and a rank drawn as text.
DECK: 32 cards, four suits of eight ranks, built in a loop.
SHUFFLE: Fisher Yates. Walk the array from the last index down to 1.
For each one, pick a random index from 0 up to AND INCLUDING the current
index, then swap the two. Do NOT sort with a random comparator.
PHASES: one variable called phase, holding DEAL, LEAD, RESPOND, RESOLVE,
REFILL, SCORE or GAME_OVER. One function per phase, plus an enter()
that runs once as a phase begins. A click on a card does nothing unless
the current phase accepts it. No nested if chains deciding turn order.
RULES: each player holds five cards. The leader plays one. The follower
must match the led suit if they hold it, otherwise anything goes. The
higher rank in the led suit takes the trick and leads the next one. Both
draw back to five while the deck lasts. Most tricks takes the hand.
OPPONENT: it plays its lowest winning card if it holds one, and its
lowest card otherwise. Keep that decision in a single function so it can
be swapped out later.
ANIMATION: the phase and the hands change first and at once. A queue of
card movements then plays out on screen, and input is locked only while
that queue is draining. No game rule may live inside an animation
callback.
MOTION: every card slide advances by the seconds elapsed since the last
frame, clamped to 0.05, so a tab left in the background does not fling
cards off the table.
PHONE: a tap on a card plays it, and
canvas { touch-action: none; } in the CSS.
CONFIG: one object at the top with the hand size, the suit list, the
rank list, the slide speed and the queue delay, each commented.
Change the rules freely. Hand size, suits, what beats what, who leads first. Leave the shuffle and the phase list exactly as they are. Those two blocks are what the rest of the rules hang off, and a later request worded as a small change is how both of them get rewritten.
Why a random comparator is not a shuffle#
The one line shuffle is a sort with a comparator that returns a random sign. It reads as even and it is not. A sort assumes its comparator is consistent, so if it says one card comes before another, it has to keep saying so. A random comparator breaks that assumption, and the result depends on whichever sorting algorithm the engine happens to run.
What comes out is a deck that has been stirred rather than shuffled. Cards tend to finish near where they started, and some orderings appear far more often than others. In a game of chance that is not cosmetic. It is the game being quietly weighted in a direction you cannot predict and cannot explain to a player.
The correct method is one pass with no sorting in it. Start at the last card. Pick a random index from the front of the array up to and including the card you are standing on, swap those two, then step back one place. When you reach the front you have finished. Every ordering of the deck is then equally likely.
One detail belongs in the prompt. The random index has to include the current position, so a card is allowed to stay where it is. Excluding it is a well known off by one, and it produces a shuffle in which no card can remain in place. That is a different distribution, and a worse one, from something that looks like a typo.
Checking it is easy and worth doing once. Deal the same deck a few thousand times and count which position the first card ends up in. An even shuffle gives a flat spread across every slot. The comparator version gives a visible shape, and once you have seen the shape you will not reach for it again.
A turn is a state machine, not a nest of ifs#
Write turn order as conditions and the first version works fine. The second version has a condition inside a condition. Then you want a rule such as following suit. Now the function that handles a click decides four things at once: whose turn it is, whether the card is legal, whether the trick is finished, and whether the hand is over.
A state machine pulls those questions apart. One variable holds the phase. One function runs as a phase begins. One function handles input, and its first act is to ask whether the current phase accepts input at all. A click on a card during RESOLVE is not a special case to be handled. It is a click the machine ignores.
The list matters more than how you implement it. A matching game needs fewer phases: DEAL, FIRST_PICK, SECOND_PICK, COMPARE, and then back to FIRST_PICK or on to SCORE. Writing that down shows you straight away that COMPARE is where a pair is kept or turned back over, and that SECOND_PICK has to refuse a card already face up.
| Phase | What it accepts | Where it can go |
|---|---|---|
| LEAD | A click on a card in the leader's hand | RESPOND |
| RESPOND | A legal card from the follower | RESOLVE |
| RESOLVE | Nothing. It runs and moves on by itself | REFILL or SCORE |
| REFILL | Nothing | LEAD |
| SCORE | A click on the new hand button | DEAL or GAME_OVER |
Two of these columns are the bug report you would otherwise have written out by hand a week later.
The payoff shows up when you add a rule. Trumps change RESOLVE. A pass or a discard is a new phase with one way in and one way out. Neither touches the other phases, and neither asks you to work out what a click handler was already doing before you arrived.
A dealing animation is a view, never the state#
A deal that plays out rather than appearing all at once is worth having. The mistake is letting the animation own the cards. Suppose a card arrives in the hand only when its slide finishes. The hand is then wrong for the length of the slide, and every rule that reads the hand has to know about the gap.
Do it the other way round. Shuffle and deal in one go, so the model of the game is right immediately. Then push a list of card movements onto a queue and let the drawing code work through it. The hand already holds the cards. The queue is only deciding where to draw them on the way in.
Input is the one thing the queue is allowed to touch. Lock clicks while it drains, because a player who plays a card part way through a deal will not understand what happened next. That lock is a single boolean, and it is the entire connection between the two systems.
- Never resolve a trick in a callback. A finished slide should do one thing, which is take itself off the queue. The moment a rule runs there, the rules depend on the frame rate.
- Let the animation be skipped. A click while cards are moving should empty the queue rather than join it. The state is already correct, so skipping ahead costs nothing.
- Time the slides in seconds. A slide measured in frames takes twice as long on a slow machine, and a slow deal reads as a hang rather than as a flourish.
- Save the model, not the motion. If a hand survives a reload, store the phase and the hands. A half drawn deal is not something to restore.
🃏Card and board games written by AI
Common questions#
Can AI make a card game?
Yes, and the drawing is the easy part, since a card is a rectangle with text on it. The two things to be specific about are the shuffle and the phase list. Left to itself a model will often produce the one line shuffle and a click handler that decides everything.
What is wrong with sorting an array with a random comparator?
A sort needs a comparator that gives the same answer every time it is asked about the same pair. A random one does not, so the outcome depends on the sorting algorithm underneath. Some orderings become much more likely than others, which in a card game means a weighted deal.
How do I shuffle a deck properly in JavaScript?
Fisher Yates. Loop from the last index down to one. Each time, pick a random index between zero and the current index inclusive, and swap the two entries. One pass, no sorting, and every ordering equally likely. Ask for it by name and the model will write it correctly.
How should I write turn order?
As one phase variable and one function per phase. Input is checked against the phase before anything else happens, so a click arriving at the wrong moment is ignored rather than handled. The alternative grows a new nested condition for every rule you add.
How do I add a rule to a card game I already built?
Find the phase it belongs to and change only that one. A trump suit is a change to how the trick is compared. A discard is a new phase with one way in and one way out. If you cannot say which phase a rule belongs to, the phases are not separate yet.
How do I write a computer opponent for a card game?
Start with one rule and keep it in a single function. Play the lowest card that still wins, otherwise play the lowest card at all. That is enough to feel like an opponent. Keeping it in one place means a better version is a replacement rather than a rewrite.
Where to go next#
A card game is one of the few genres where a finished thing can be small. One hand, one opponent, one rule worth thinking about. Publish it and the page keeps the model and the prompt beside it, which is the part the next person reads.
The method underneath is in how to make a game with AI. If you want a human on the other side of the table rather than a function, the two player guide covers sharing one screen. Phases and rule layers go further in the strategy guide, and fixes for particular faults are in the prompt guide. The card games here show which rule sets survive a stranger playing them.
Built something? Put it on Agent Games.
Publishing takes a file and a minute. Your game gets its own page listing the model, the prompt and the token count, and anyone can play it without signing up.
Read next