On April 22nd 2024, a screenshot from the lua code for the hit new indie game Balatro, a poker roguelike deckbuilder, was posted on Twitter, and people collectively lost their minds.
There is a 4717 line file called card.lua
that contains all of the gameplay logic. A lot of people who have not done a lot of game development are laughing at this.
Rapid Prototyping
The thing is, games do not pop into existence fully formed. There’s a pitch, sure, but that’s just an idea; a space where a game might come to be. In order to discover what your game might be, you need to experiment, and in order to do that, you need to write code that will allow you test ideas quickly and easily. The process of doing this is called “Rapid Prototyping”, and it’s a vital part of the process of exploring your game idea and finding the details of the reality.
A lot of things that sound good actually just aren’t that fun in practice. Other times, things you never imagined in the pitch come to you while you’re experimenting and you pivot. Being able to pivot your design quickly is key to being able to make those discoveries. The real skill of gameplay programming during R&D is being able to find a balance between “what lets us test ideas quickly” and “what limits our ability to move forward”.
If you actually look at what this code is doing, it’s not something a simple “switch” case could necessarily do, and Lua doesn’t even have a switch case. This block of code is looking for a single sub-block to return, but the value match sometimes comes with one or more validation checks that prevent it from entering the sub-block if the preconditions aren’t met, and the work performed in each sub-block is not necessarily static either. It’s setting a couple of different variables and performing per-card functionality ONLY once the card precondition has been satisfied. A look-up table (LUT) alone won’t satisfy this as it must have configurable precondition checks and post-check actions. You are routing. You need a system.
Systems design can be tricky. Every person’s strengths and weaknesses as a developer and as a programmer will give them different approaches. Systemizing too early has several drawbacks:
You need to commit to a significant amount of development before you can start designing
Increasing design reliance on engineering resources to perform basic experiments if the system is not appropriately extensible
Failure to correctly predict where design will need to go can limit the execution options your designers have and shoehorn them into behaviors supported by the system, affecting design outputs
You need to make sure that you provide tools that allow designers as much freedom as they can have without breaking fundamental engineering pillars, and they need to be able to experiment quickly. When you systemize, you add constraints, and these constraints can mean that your designers either can’t do what they want without help, or worse; that they start working around your systems in ways that will cause later breakage as your assumptions are invalidated.
While I’d feel confident building a card system that could extend to the necessary potential outcomes while leaving room for experimentation, I come from a data-driven background so that’s the methodology that makes sense to me, and I have a lot of experience coming at problems from that angle. I’ve worked on a lot of F2P live service games so I’m always going to be thinking about maintenance and effectiveness, and it’s trivial for me to write those kind of systems.
But even then, I don’t always write them as a first option. Outside of my day job, I help out on indie games. The other day I was making footstep audio, and the first-pass version was to simply place sound emitters approximately where the player character’s feet are, and trigger some manual logic in the animation blueprint to fire events when the animations stepped. It passed through a series of if/then branches to figure out which footstep sound to play, and voila.
The sound designers are unblocked now, and can work on creating and tuning the footstep sounds as they see fit. It will not work for non-bipedal creatures, or anything that doesn’t share the humanoid animation blueprint. It will also need refactoring if footstep location needs to accurately reflect bone placement. But presently, the game does not need those things, and if either of them become a priority, that is the trigger for refactoring. If neither of those things occur, that simple branching logic will probably ship, because it is good enough.
Good Enough
People talk about “good code” but what does that mean?
In junior infrastructure interviews, I like to ask the following question.
“You have a task to write a program to retrieve, process, and analyze game data logs (telemetry) and upload the output to a server. There are two approaches you can take to this task.
The first (hacky) will take an hour to write and four hours to run.
The alternative (structured) will take four days to write and ten minutes to run.
Which approach should you take?”
The answer, is that there is not enough information to answer the question, and in fact to ask two other questions:
How often does it need to run?
What does execution time scale off, and what is the rate of change of that thing?
What are the repercussions of it not executing in time / can two of them run concurrently?
If this task only needs to run daily, and it scales off records which are doubling every 12 months, then the hour-long hack version can happily be in place for 3+ years before needing to be replaced. If the output doesn’t need to be available the same day, it may never need to be replaced.
Conversely, if two instances cannot run simultaneously then it MUST be replaced after three years. If you can’t even approximately guess how the scaling factor will change, and it could potentially 6x overnight, then you MUST write the long version.
Also .. do you have four days of development time available to prioritize it? If nothing else is on the table or urgent, maybe you just do the good version straight away and never worry about it again.
It’s the dreaded senior answer: “it depends”. Code needs only be good enough for its requirements, both operational and developmental.
What Is Good Enough For Your Game?
Same answer again. It depends. Apex Legends shipped without any skill-based matchmaking. Respawn had a skill-based matchmaking engine for Titanfall 2 they could have used for Apex Legends, and decided not to. When it became apparent that new players who found matches too difficult OR too easy were quitting more than players who had an average challenge, they successively shipped small changes that siphoned these players off to give them an appropriate challenge, and retention improved. It wasn’t for a full three years that they hit a point in the game where it was worth bringing in a full skill-evaluative model, and even then they found that a full performance analysis produced about the same effect as rating players on ‘damage dealt’. Because Apex has a high TTK and is latency tolerant, it was good enough.
So was this good enough for Balatro? All signs point to “yes”. The code isn’t running frequently, it’s easy to read, easy to maintain, and the game seems to be reliably handling 50+ status effects active simultaneously. In fact, I can’t think of any bugs I’ve seen anyone talk about that might be related to this code.
Would I have done it that way? No.
Would I, upon reaching the inflection point where there’s been significant investment in that method, advise refactoring? Also no.
It’s not efficient from a Big-O perspective, but why does it need to be? It’s within spec, and it was efficient from a design perspective because it allowed for rapid prototyping of cards before the design was locked in. And it scaled well enough to last to completion of the game. The code did what it needed to do at every stage of development.
So when people say it’s “bad” and say it should be [alternative solution]
, I have to ask; why do you need to? At any point in a development cycle, you should be doing the most valuable work that you can. At what point would refactoring that be the most valuable work, and what would be the benefit?
These are questions you should be asking yourself about every piece of work you commit to. This is not a college assignment where you get marks for approach. You are shipping a game. Do the work that enables you to achieve that.
Until next time.
// for those we have lost
// for those we can yet save