Distilling the Essence Out of TTS’s Lua Scripting

Image may contain: Seth Berrier

For this post, I’ve invited Seth Berrier, to write a devlog about scripting in Tabletop Simulator for Distilled. Seth is a professor at the University of Wisconsin-Stout, and a good friend (and gaming buddy) of mine. While I’m designing for the eventual physical version of the game, Seth has made the design and development of board games in a pandemic actually possible. If it wasn’t for Seth, I’d likely be 6+ months behind. Without further ado, here’s Seth!

How it got started

I first played Distilled back in the fall of 2019 with Dave’s prototype cards and a few of our tabletop gaming regulars. It was a hoot!  I kept locking them out of the warehouse (an unfortunate old way you could abuse things) and another player caused a perpetual drought keeping us from gaining any water cards.  Remembering those simple and somewhat convoluted abuses we came up with on the spot that night, it is a real testament to Dave’s dedication to balancing and focus on core mechanics that today’s Distilled affords none of those shenanigans.  And of course, it is still a hoot, just a much more fair hoot!

There was so much potential in Distilled, and like all good concepts, we walked away planning out strategies for the next playthrough (assuming Dave would thwart our good-natured abuses with even better design next time).  But, that was the only time I would get to play it in person.  A few short months later, the coronavirus hit the U.S. and in-person game night became impossible.  We watched all our tabletop adventures (Distilled, Gloomhaven, Star Wars: Imperial Assault, a just-started round of Tainted Grail) all come to a screeching halt!

But you can’t keep down a good idea!  While I had no experience with it, Dave mentioned in passing that he was experimenting with bringing Distilled into Tabletop Simulator (TTS).  I knew of it but had not tried it (or even purchased it yet).  Being a programmer by trade, and very much wanting to see Distilled succeed, I sent a short message to Dave via FB Messenger:

Prelude to a summer of fun in TTS

… the results of this innocent message are now up on the Steam Workshop for everyone to enjoy!  Seven and a half months of hundreds of messages and notes, working 100% remotely, coding, learning Lua, breaking GUIDs, finding and squashing bugs, and dozens of virtual playtests and redesigns to make Distilled virtual.

First explorations (cards and collections vs decks)

Like many great adventures, it began simply.  I wanted to prove to myself that I could do something basic and useful in TTS, that my coding skills would transfer over to there, and that it would keep me challenged.  Without that, I probably wouldn’t see it through, I know myself.

Distilled is a card game, and I was already finding that cards were hard to work with in TTS.  They have a plethora of clever ways you can manage them but it never feels as natural as in real life, the sensations of shuffling decks and cards flying around a table as you deal them were completely absent.  So I set out to simply recreate some of that sensation.  Could I make it as satisfying as a fresh deck of waxed bicycle playing cards flying through your hands?

The proof of concept that convinced me this was worthwhile.

This was my first test, and while it was very different from a real deck, it was strangely satisfying in its own way!  The simple but intuitive physics of TTS combined with the SFX of cards flying around and plopping satisfyingly onto the table felt a bit magical. Seeing decks shuffle and flip themselves felt polished and compelling and well worth the effort it took.

And by effort, I mean dealing with some peculiar, but very necessary constraints imposed by the Lua scripting system.  TTS aims to be a general-purpose tool for others to create game experiences.  And while a few basics are included (chess, checkers, solitaire, poker) the real modern game experiences they wanted to enable were much more complex, ranging from incredibly detailed scripted fantasy experiences (like Imperial Assault) to a simple game of Uno.  It was all there in the Steam workshop (likely breaking intellectual property copyrights, but there, none-the-less).

Thriving in Constraints

The Lua application programming interface (or API) provided by TTS is robust but, like many high-level scripting interfaces, imposes a few weird constraints that one might not be used to if you do a lot of lower-level programming.  Every object in the world has a GUID (globally unique identifier) and it is clear that this was intended as an easy way to grab a reference to an object and do anything to it you wanted.  Once you had the GUID, you could do everything the player could in the game with a single function call: move it (smoothly) across the table, shuffle it if it was a deck or bag, flip it over with a nice animation, drop it on other objects to automatically form a group, lock or unlock it in place, and it all started with a GUID.

The problem was, those GUIDs were unstable!  Every time Dave updated a component, it would receive a brand new GUID and break the scripts.  I also learned that an object inside a bag or deck can’t be found by its GUID until you remove it from its container (still just one function call) and once removed, it takes a frame for it to come into existence and then more frames (potentially several seconds) before its textures are loaded and it has a valid position and size.  TTS was being very careful not to load too much data at once but this was causing unexpected delays later when that data was finally loaded.

I started to see a lot of little bugs and errors that were somewhat familiar from my time programming modern full-stack JavaScript servers and web pages.  Concurrency errors, caused by expecting something to be available before it really was.  While modern JavaScript has some very slick tools to help you deal with this (Promises and Async/Await) Lua had none of this.  It was up to me to recognize when time was needed and to wait an appropriate amount before continuing to fling things around.

An early, pre-1.0 version of Distilled with scripted setup only.

And that was when I became really intrigued!  TTS’s Lua API allowed for concurrent functions that could yield execution for a frame: a coroutine.  It took a lot of time to figure out when these were necessary and to make good use of them but they solved almost all of my problems once I was comfortable.  These co-routines were also frustratingly limited in that they could not receive ANY parameters when called, so passing data into them was impossible.  But such constraints can be dealt with if you plan carefully.

Iteration One – Basic Setup

I took what I had learned with my proof of concept and applied it to the setup phase of Distilled.  While it was possible to just have it load pre-setup, ready to go, there was no randomization, it was just pre set up one way which hurt replayability.  Also, all four players would be seen even if you didn’t have four at the table.

So the first thing I did was script the setup: all decks would shuffle, cards would deal to players that were seated only, and extra player-boards and tokens would be automatically removed so as not to clutter the table.

Scripted game setup in Distilled TTS 1.0

It was incredibly rewarding!  All the cards magically flying around the table, decks shuffling and flipping as if by magic, extra components disappearing to the ‘box’ without any need to tell the game; it restored some of that tactile sense of in-person gameplay despite the virtual setting.

Dave’s maddening pace of changes was breaking the script pretty frequently and I was hacking away placing everything in the ‘global’ object not caring for expansion or complexity, just learning and solving problems as you often do when you are rapidly prototyping.  It felt good, but I knew it wouldn’t last for the long haul and with Dave planning to launch on Kickstarter in the Spring of 2021, it became clear that this was a long haul job!

I started to refactor the code separating concerns out into the specific objects that were most related.  All market decks and actions were owned by the market board and all scripting for those would live in there.  Functions to deal cards to players and remove extra boards moved into the player boards (and had to be copy-pasted four times every time it was changed, uff-dah).  I started expanding the precision of it all, laying out snap-points programmatically and uniformly for every player mat and dealing with those precise positions.  I also added buttons to refresh cards after they were purchased in the market so anything tedious was made easier.  If it was easy in real-life, it should be easy in TTS — that was my guiding philosophy.

Buttons to refresh cards that were purchased from the market.

And the more I changed, the more Dave’s updates would break the scripts!  The delicate GUIDs kept changing and I would frequently get messages like this:

Dave breaks the game by bringing in all of Erik’s brilliant artwork.


Dave was also formulating big ideas to completely up-end the gameplay.  It all culminated in a massive redesign where all the lessons I had learned doodling around in the first prototype came in handy.

Iteration Two – Drafting

Distilled started as a deck builder: purchasing cards from your current economy in your hand from the market and building up what you needed for a specific spirit.  This was playing slow on TTS and was receiving a lot of critical feedback from playtesters so Dave decided a big change was in order.  He wanted to switch from deck building to deck drafting!

Much faster setup in v2 but still very satisfying.

We formulated a plan, I tried to decide how to make the act of passing cards left or right less awkward (so simple in person, but it was awful on TTS), and I took my refactoring and stripped it down to the bare essentials.  It was a big re-working of the underlying scripts, a true v2.0, and I reveled in the chance to learn from my mistakes the first time around.  This is always a satisfying experience, going in and laughing at the crazy things you did when you didn’t know any better.  Even more satisfying to fix it and see the gains in code simplicity and robustness.

This was only my second prototype in TTS so I still hadn’t learned everything but I fixed a few key mistakes: all my GUIDs were now sitting at the top of the scripts where they were used.  When Dave would break something, I could fix it in a few minutes now, recognizing which GUIDs were broken quickly by logging them to the console instead of just letting the game crash.  I made the code even more modular, moving more scripting into the secondary draw deck mat, and I gave up on trying to ‘instance’ the player mats and decided to just live with coping and pasting the code every time I changed it: tedious yes, but it worked and the TTS Lua object interface simply does not afford class-like object instancing in any robust way (I tried).

Evolution of the setup GUI: first one in top-left, below it is v2, right two are current


The end result was different from the first round.  There was now a customizable GUI with lots of options to scale the game difficulty and to help Dave bring in components that were still being prototyped.  I put some magic variables in there too that helped Dave test different card counts and market sizes and such, things that were super easy to script but tedious to do by hand.  It was feeling more and more polished and professional, just by working from the lessons I had learned from the first, deck builder prototype.  Dave also requested a river mechanic for the market board that was refreshingly simple to implement with all I had learned.  It felt like I was reaching some kind of TTS Lua maturity now.

It’s never a good sign when Dave wants to chat to run something by you …


But Dave’s relentless pursuit of perfection wasn’t done!  Countless playtests as ‘spiels’ and online cons were showing that the core fun was everything AFTER the drafting phase.  Letting players buy what they needed with their spirit profits was sufficient and building a large initial deck through deck building or drafting was simply not necessary.  And version 2.0 of distilled (which never even made it up onto the steam workshop) was obsolete!

The only way you’ll ever see the drafting unless you hack the scripts.

Iteration Three – Precision and QoL

Luckily for me, all the pleasure of refactoring and fixing your mistakes was going to come back around again very quickly this time!  Rather than completely remove the drafting code, I left it there and disabled it with a global flag.  Those who feel nostalgic for it could rummage around in the LUA and turn it back on (although you should expect it to break things as it is just a remnant and hasn’t been touched since early October 2020).  The prototype that had been dashed together in the spring was getting a second distillation towards perfection and I wasn’t about to let any low-hanging fruit go unpicked.

The constant breaking of the GUIDs was getting even worse with every change Dave was making and it was taking me longer to fix the scripts each time just because of their inherent complexity.  I strove to add some robustness by implementing a very large centralized system for retrieving objects.  It was filled with huge lists of objects associating each deck, player mat, token, recipe card, and label with BOTH a GUID and an in-game name.  I went through and named everything that we need to reference from the scripts (many were already named) and provided myself some easy to call functions that would not just crash when the GUID was wrong but would fall back to the string name instead.  It would also log information about any broken GUIDs so I could fix them faster (listing the proper GUID right in the log).

The market river mechanics in action (I’m particularly proud of this one)

It was significantly more robust to Dave’s changes and got my time to fix things back down to just a few minutes.  A great quality-of-life improvement for the distilled team: Erik who was churning out the art at a breakneck pace, Dave who was sifting the chaff from the wheat in the design, and my scripts trying to help it all shine as best it could online.

I also took the opportunity to beef up the customization GUI with lots of helpful feedback from Dave, and to lay down precise snap points across the board only when they were needed.  Rogue snap points for players that weren’t seated or decks that weren’t enabled were gone and these little polish points were very satisfying to engage.  Very few of the satisfying sensations of cards flying around in the first proof-of-concept had remained.  Instead, things went straight where they needed to and wasted little time, a precious commodity to everyone that I did not want to take for granted.

The final version of setup, complete with authentic recipe cards delivered to your door.

But those strangely satisfying sensations were still there, a surrogate for the scent and feel of a freshly opened waxed cardstock or punching out chits from a perfectly perforated board.  It was concentrated to its own essence through Dave’s tireless pursuit of perfection, Erik’s brilliant and unique artwork, and the learning process of my own journey through TTS Lua scripting.  And I am now feeling the empty nest sensation of a programmer that has done their job well.  My creation sits in the hands of players, waiting for bugs to be found, but other than an initial flurry of small problems, no exterminating has been necessary!  I am no master of TTS Lua but I have tamed it enough to feel confident with it and, being a teacher, I am eager to pass on this knowledge now to the next generation of tabletop game designers.

2 thoughts on “Distilling the Essence Out of TTS’s Lua Scripting

  1. Jim says:

    Felt like I was reading a summary of my own adventure for the past 9 months… been developing a TTS mod for Nightingale Games’ War Room and had to learn Lua scripting from scratch. I even wrote my own promise library to improve on the Wait API that TTS provides! Let me know if you want to swap notes sometime 🙂

    1. distilledgame.com says:

      Thanks Jim! I’ll pass on this comment to Seth, as I’m sure he’d love this!

Comments are closed.