How TIS-100 Made Me a Better Software Engineer

Angel Irizarry
Apartment List

--

How do you get better at software engineering? As with most things, it’s about practice. Lots of practice. But accumulating 10,000 hours of code-writing can only get you so far. The key is deliberate and guided practice. In software engineering, deliberate practice could mean designing a system under a very narrow set of constraints, optimizing for a particularly scarce resource, or just working through a hard problem. You can achieve guided practice by seeking feedback in the form of code or design reviews. If you want to get better, you should always look for opportunities in your career and personal projects to hone these skills and get solid feedback.

Or you could go home and play TIS-100.

A co-worker recently recommended this game, assuring me it would be up my alley. Given the name alone, which stands for Tessellated Intelligence System, I suspected this was not going to be a typical game. Launching the game takes you through a simulated boot sequence reminiscent of an early 1980s computer, followed by a screen showing the list of levels, and a button to optionally open the reference manual. Choosing to open the manual immediately earns you the “RTFM” achievement, which is a nice touch. This is a programming game, no doubt.

A typical level consists of at most 12 input boxes, each of which represents a CPU node, and is a blank canvas meant to be filled with lines of assembly code. A typical level will task you to write code to process streams of data to produce new streams of data. One level asks you to read a stream of integers, and output the minimum value and maximum value from that stream. Yes, you could write this in one short line of Ruby, but that wouldn’t make for a fun game. The constraints imposed by the game’s limited instruction set, memory, and communication busses not only add to the appeal of the game, they also teach you how to design better systems.

A typical level in TIS-100

System Design

A for-loop in assembly

An early level in the game lends itself well to using nested for-loops. In assembly, a for-loop can be approximated by decrementing a counter, checking if that counter has reached zero, and conditionally performing a jump that starts the loop over again.

Nesting this construct inside of another one would trivially solve the challenge, but any attempt to do so reveals one of the fundamental constraints of the game — each node can have at most 15 lines of code, including comments. Nested for-loops simply don’t fit! To add insult to injury, each node only has one register — and a backup — not unlike the M+ buttons on vintage Casio calculators.

To work around this constraint, you can split up your algorithm into little modules, and each module can run on a separate node. One node can use all of its allotted space to perform the work inside the nested loops, while one or maybe two neighboring nodes can keep track of their respective place in the loops. Now you can focus on working on each node separately, ensuring it can perform its task and communicate to its neighbors effectively. Each node also gets its own memory register, so, for example a single node can be entirely responsible for storing the index of the inner loop, incrementing it, and reporting it back whenever it’s requested.

The epitome of microservices

By forcing me to do this exercise in each level, the game has made me better aware of real systems that could benefit from being broken down into simple modules, or functions. When modules are designed so that they do one simple thing really well — and ideally written in under 15 lines of code — they are easier to reason about, easier to test, easier to maintain, and more likely to be reusable.

Distributed Computing

The nodes in TIS-100 can only speak to their four neighboring nodes, and they can only communicate by passing single integers. Real systems are not usually limited by this constraint, and it is all too easy to make large, complex payloads that both the sender and the receiver have to fully understand. But, by being forced to send and receive single integers, I was able to see the beauty and benefit of simplicity. The contract for communication is clear, and there’s no danger of receiving invalid values. Because nodes can only communicate with their immediate neighbors, the dependency graph is also necessarily simple to reason about.

While this game doesn’t touch on the scarier aspects of distributed computing, like consensus algorithms and fault tolerance, it has taught me that a solid foundation of simplicity in software design can go a long way in making these more advanced topics more manageable. If a module requires two or more disparate inputs to do its work, maybe that is when you should consider splitting the module into multiple modules, and make one that combines the results, not unlike Map Reduce. By splitting up the work in this way, you also get the benefits of parallelism for free.

Performance Trade-Offs

TIS-100 wouldn’t be much of a game if there was no way to assign you a score and a position in the leaderboard. The game measures and ranks your solutions in three different ways: instruction count, cycle count, and node count, with the goal being to minimize these values. Thankfully, you can optimize these metrics independently, and write three versions of your program that each focus on optimizing one metric.

Don’t make fun of my score

Starting with a program that minimizes one metric and converting it to a program that minimizes another metric is an exercise in performance engineering. Working through each problem, I understood how I could lower the number of cycles by splitting up the work of one node into two nodes that run in parallel. I saw how I could lower the instruction count by doing something clever that lowers readability and adds cycles. Real-world performance engineering is all about trade-offs, and it’s readily apparent while playing this game how you can use more memory to make things run faster, or write more lines of code to use up less memory. While TIS-100 tasks you with optimizing for the extremes — lowest node count, at all costs, for example — in the real world is it up to you to determine which metric is important to you. But seeing how these trade-offs are made in a controlled environment like this game is a valuable skill that can help you make more informed trade-offs in your engineering career.

You Should Play

I’m not yet done with this game. The last row of challenges are proving to be particularly difficult, and I have yet to explore the playground where I can potentially make my own challenges. But I’ve already begun to apply the lessons of this game to the real-world. I encourage you to try out this game and see for yourself how simplifying and splitting your modules, keeping your communication protocols simple, and making thoughtful performance trade-offs will make you a better engineer. And don’t forget to RTFM.

Angel Irizarry is a passionate full-stack software engineer at Apartment List, with 10+ years of experience building and breaking things. Originally from Puerto Rico, he is now based in San Francisco.

--

--

Angel Irizarry
Apartment List

I don't separate my interface from my implementation