bruno

"mr storylets"

writer (derogatory). lead designer on Fallen London.

http://twitter.com/notbrunoagain


THESE POSTS ARE PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE POSTS OR THE USE OR OTHER DEALINGS IN THE POSTS.


Bluesky
brunodias.bsky.social

bruno
@bruno

Hm, an approach I'd be likely to take coding something like this would be to write a code generator. People are arguing about the Lua files packaged with Balatro; do they even know if it's handwritten code?


You must log in to comment.

in reply to @morayati's post:

sorry to bother, could you repost the contents of the thread in some way. there’s literally no way to access twitter anymore without an account and i don’t have one

no problem! (apologies, was in a meeting)

long text below


"There is some confusion about my meaning, so let me write yet another thread expanding on it that most people replying won't see. I don't just mean in the sense of "it's an indie game, fuck it, if it works, ship it". I mean in the sense that this is stylistically correct.

Criticisms of the code mostly fall in two camps: performance, and style. Performance is irrelevant. It's written in the interpreted language Lua, it does multiple hash table lookups per if, but that doesn't matter, it runs once per frame, computers are fast.

For style, we have to focus on what it does. Balatro is a game with lots of unique playing card Jokers, each with its own ability. As a result, it has what I called "bespoke" code; each such unique joker need custom code to implement the unique behaviors for that joker.

Speaking in general, not Balatro, this is a general pattern you see sometimes in games (and probably less so in other software!), lots of unique objects each with unique behaviors. (This is very common in text adventures, hence the other thread.) Typically in this pattern, each object has multiple behaviors. Let's take a hypothetical platformer; maybe there's a "function" for tick(), that is time advancing, collide_wall(), collide_object(), maybe a special collide_player(), maybe draw().

The classic instinct is to use type-based polymorphism; each of these "functions" becomes a method and there's a separate method for every object. In a classic OO system, this would require you to create a separate class for every Balatro joker. In most text adventure languages, you could code these functions directly "on" the objects themselves without needing a separate type. In something like Python, you can make objects with fields and put a lambda into each field. In each of these cases, we have N objects and M "messages" and N*M methods (unless we can inherit or share somehow).

Which leads me to where I wanted to start but I knew people would be clamoring about OO if I didn't open with it: what we actually have are M behaviors and N objects and a grid or matrix of M*N "actions". Traditional OO logic says you should logically group this into N objects, each with M behaviors, where those M behaviors for each object are located in close proximity in the code. But there is an alternative style: M behaviors, each for N objects, where those N objects for each behavior are located in close proximity.

Now, in certain kinds of OO programming, it may seem more logical to group behaviors than to group objects. For example, a given class might want some private helper methods that are used by multiple behaviors for that class, so grouping them makes it more convenient to share elements of the behavior.

I put it to you that, in a lot of cases that follow the pattern I'm talking about--in games like Balatro or Storyteller, not in text adventures--the behavior simply isn't complex enough to typically need that much code sharing per-object. But, with a large collection of unique objects, you may often have objects with identical or similar actions for a given behavior, and grouping by behavior--all objects for one behavior together--makes it easier to share that logic. It may also be the case that it's simply easier to think about: looking at examples of how other objects handle the behavior makes it easier to write an action for how a similar but different object handles the behavior, so having them handy makes it faster to write new objects.

Of course, most languages don't have any facility for factoring the MN matrix of behaviorsobjects so they're grouped by objects for one behavior, and so you have to write switch statements or if() chains. And maybe that seems clumsy or cut-and-pasty to you. But on the other hand, some OO languages force you to do a lot of ugly boilerplate (e.g. python), and if even the non-class based lambda approach for python is still going to be a little clumsy, enough that I don't think the if()-chain is a meaningful disadvantage.

So:

  1. performance of this code is fine
  2. in general, grouping by behavior can be beneficial for heterogenous but relatively simple objects, so it's good style/structure here
  3. if it works, fuck it, ship it"

in reply to @bruno's post:

I imagine it's handwritten simply because given the tiny variations in like every part of the statement (name-only/name and X/name and y in the if statement, a_mult vs a_xmult, various permutations of the mult_mod/Xmult_mod) it'd be tough to make a generator that wasn't about the same amount of time inputting as just typing it out. But who can say! Programmers sometimes do love over-automation.