Stacking Trouble

February 24, 2026
splashscreen gameplay 1 gameplay 2

Stacking Trouble is a game that (poorly) attempts to re-create Stack Attack game that came preloaded on old Siemens phones. The goal of the game to organize falling crates into a row by pushing them. Once the bottom-most row is filled, it is discarded and points are awarded.

Version 2 of the game is an attempt to polish the game and introduce new features that were not implemented due to time constraints.

  • Power-up crates are introduced that either give player health or double his movement speed. TNT crates that destroy crates within certain distance are also introduced.
  • Proper graphics are implemented (no more RlDrawRectangle with random colors, yay!), including player animations and explosion effects.
  • Menu system with ability to view controls, change game difficulty or view in-game tutorial.

This game was originally made for Langjam Gamejam 2025, taking place between December 14th and December 20th. The goal of the jam was to make a programming language first and then write a game in said language (all in 7 days). Stacking Trouble is written in Bla - dynamically-typed interpreted language with its syntax very much inspired by Python. Golang was used for the compiler/interpreter and RayLib is used for rendering.

Game's source code is available on GitHub.

The Language

The game is written in a language called Bla (I didn't have a good name for it) which is very much inspired by Python... without any conveniences of Python. The language has the following constructs:

  • Boolean, integer, float, string primitive types.
  • List type and its related operations: accessing an element of a list, list assignments, appending to a list and accessing length of a list.
  • Objects and its operations: set and retrieve object attributes.
  • Various expressions - boolean and/or/not, comparison operators (e.g. <, ==, etc), arithmetic operators, and ternary expressions (e.g.a if a else b). Short-circuited evaluation is applied to and/or operators. That is, and gets compiled to b if a else a and or gets compiled to a if a else b.
  • Assignments, while statements, if statements (with optional else), print, assert and return statements.
  • Functions, function calls, and function references. Reference to a function can be created by prefixing a valid function name with & symbol. In combination with objects, it is possible to write some basic object-oriented code.
  • Global constants which are defined using global keyword and ability to read them using global expression.

Actual game's source code can be found here and (rather incomplete) compiler testing suite containing sample programs can be found here.

The Interpreter

Interpreter is essentially a stack-based virtual machine that operates on "bytecode" (using that term loosely here) and consists of four main parts:

  • Program counter: index of the instruction being executed in the instruction list.
  • Evaluation stack for evaluating expressions and passing argument during function calls.
  • Call stack for handling function calls. It stores a return address and maintains a hash table of local variables and their values.
  • Global constant pool.

All values are represented with value sum type (implemented as interface in Golang), each variant storing typed value. All value variants implement truthy method that is used for branching which makes statements like if [] possible. The following value variants are implemented:

  • none for representing nulls.
  • int64value for storing integers.
  • float64value for storing floats.
  • boolvalue for booleans.
  • functionAddressValue for storing function references.
  • stringValue for strings.
  • listValue for dynamically-sized lists, storing value instances.
  • objectValue for objects. Objects are simply a table containing field names and value instances. objectValue itself stores a pointer to that table, hence objects are passed around by reference.
  • textureValue for storing rl.Texture structure, needed for calling Raylib's texture drawing functionality.

In summary, every possible value in Bla is boxed.

Instruction Set

Instruction set if pretty straight-forward.

  • Add/Sub/Lt/Eq, ... for arithmetic / comparison operations. These instructions consume two values from the evaluation stack, manipulate them in some way and push some value onto the stack.
  • Neg and Not for negating numeric values and booleans. These instructions consume one value from stack and then push one value onto the stack.
  • PushInt(int), PushFloat(float), PushTrue, PushNone, PushFunctionAddr(int), ... for pushing values of particular type onto the stack. Some of these instructions require an additional operand. For example PushInt 10 pushes IntValue onto the stack.
  • LoadVar(name) and StoreVar(name, value) for reading and storing local variables. These instructions require an additional operand - a variable name (name) to read or store. These variable names are essentially a key in a local variable table (i.e. is stored in a call stack frame). For example, LoadVar x would read x from the top-most call-stack frame local variable table. If no variable with such name is present in the table, the interpreter crashes.
  • GetGlobal (operand) for reading globals, and SetGlobal (value) for storing globals. Language does not expose syntax for writing globals, however; and is only used in the $_entry function to initialize globals, before calling main.
  • Conditional branching BrIf(offset, value)/BrIfNot(offset, value) and unconditional branching Br(offset) instructions. These require an operand offset of type int which is an index of a target instruction to jump to. Conditional branching instructions also consume some value from the evaluation stack, identify if it is truthy and branch if necessary.
  • Call (int) instruction pushing a new stack frame on the call stack, sets its return address to be program_counter + 1 and jumps to an offset as indicated by the operand.
  • CallVirtual behaves very similarly to Call but it reads an address to jump to off the evaluation stack. Value that is consumed from the stack must be functionAdressValue.
  • Ret: sets program counter to the return address as stored in the top-most stack frame in the call stack and then pops said stack frame.
  • SubscriptSet and SubscriptGet instructions for accessing elements of the list. These consume values from the evaluation stack.
  • ObjectFieldSet and ObjectFieldGet instructions for accessing attributes of an object.
  • CastInt and CastFloat instructions for casting arbitrary numeric values to int64value and float64value, respectively.
  • Various built-in (e.g. random number generator) and Raylib functionalities are exposed as instructions. For example, rl.DrawTexture is exposed as RlDrawTexture instruction, consuming arguments from the evaluation stack accordingly. During code generation phase, these built-in functions are identified via reserved symbol table and appropriate opcode for the function is emitted.

Calling Conventions

Since the language is dynamic and has function references, we need to verify if a function was called with correct number of arguments at runtime. To facilitate this, in addition to pushing arguments onto the stack, actual number of arguments is also pushed onto the stack. For example, the following function call a(1, "string") would result in the following evaluation stack configuration:

[ ...<omitted>... | int64Value(1) | stringValue("string") | int64Value(2) ]
                                                            ^ top of the stack 

In summary:

  • Caller pushes arguments onto the evaluation stack, pushes argument count on the stack, and calls the function. This pushes a new stack frame onto the call stack.
  • Callee first asserts that the argument count is correct using AssertArgCount(int) instruction by inspecting value on top of the stack. If successful, the value is popped from the stack; error is raised otherwise. Remaining arguments are consumed from the stack and assigned to corresponding local variables.
  • All functions return some value (none by default) and end with Ret instruction which pops top-most call stack frame and updates the program counter.

Conclusion

All in all I'd consider this project a partial success: I have something that works and functional, although the version of the game that was submitted to the jam had to be simplified to meet the deadline and the resulting language is pretty much textbook. Here's add odd mix of things that I wanted to do and a wishlist for the next jam (if I were to ever to one again):

  • I originally wanted to implement interpreter in C but by the end of day 1 (after getting basic parser to work) I realized I wouldn't be able to meet the deadline (since I'd also have to implement my own memory management solution) so I went with Go instead.
  • Writing a tokenizer and parser by hand was a bit too time consuming. I could have used some parser generator like yacc but it had been a while since I used one and I really didn't want to deal with shift-reduce conflicts at the time.
  • The language is lacking some sort of module/importing logic. Version 2 of the game is sitting at about 2800 lines of code, making navigation around the source file a bit tedious.
  • I opted to expose built-in functionality as actual opcodes in the instruction set which is fine for a simple game like this (we are only using a tiny subset of the Raylib after all) but doesn't scale well. Should look into implementing proper foreign function interface (FFI) next time.
  • Art assets for version two of the game took about a week. I had no idea what the game would look like so it took about 4 days of messing around in Substance Designer until getting something I liked. There's no way I would be able to complete art for version that was submitted for the jam.
  • All art assets are stored in a directory on a file system and in the game's initialization code we load all assets via RlLoadTexture built-in. Perhaps storing these assets in a some sort of container format would be a better approach (think Doom's wad file).
  • I should have figured out how to properly compile the game on Windows ahead of time (as it requires CGO). Handling all this compilation/packaging stuff two hours before the deadline was not fun.