I did Advent of Code on a PlayStation

I did a very stupid challenge this December - I did Advent of Code in Dreams.

Dreams is a PlayStation game creation platform by Media Molecule, the studio most famous for the LittleBigPlanet games. Users can create and share everything from games to animations to interactive art. It’s truly amazing what these tools can do - I mean, just look at this stuff.

Unfortunately I am no artist. But I am a programmer, and Dreams has a robust “logic” system designed for game scripting. When I bought Dreams a couple years ago, I discovered that this logic system is a capable visual scripting engine with a very thoughtful design, and I was quickly able to produce some fun results, culminating in my magnum opus: a toy LISP interpreter.

This proved to me that Dreams was capable of just about anything. And as the leader of the Handmade Network, I’ve heard a lot of discussion over the years about “visual programming”.[1] So this year, I decided to give visual programming a serious try. And what better way to experiment with a new programming system than Advent of Code?

By “discussion”, I primarily mean “I am a Real Programmer and would never dare touch a mouse”, or “I have only seen visual programming systems for children, therefore all visual programming systems are toys”, or “but Fred Brooks said that programs cannot be visualized”. Have some imagination!

The results

I completed 15 of the 25 days of Advent of Code. By “completed” I mean “got the example working”, because I didn’t have any way to copy-paste my actual puzzle input into Dreams. Also, Dreams doesn’t have lists.

In the end, Day 16 was too much - an optimization problem that requires recursion, Dijkstra’s algorithm, and N! runs to determine the optimal path. I spent three days trying to solve it and eventually had to admit defeat.

You can see videos of each solution on the calendar below. I’ve highlighted my favorite results! (Each video is timestamped to a good part.)

Calorie Counting

Rock Paper Scissors

Rucksack Reorganization

Camp Cleanup

Supply Stacks

Tuning Trouble

No Space Left On Device

Treetop Tree House

Rope Bridge

Cathode-Ray Tube

Monkey in the Middle

Hill Climbing Algorithm

Distress Signal

Regolith Reservoir

Beacon Exclusion Zone

Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

Calorie Counting

Rock Paper Scissors

Rucksack Reorganization

Day 1: Calorie Counting

1:45:00 vs. ~15:00
/ /

As expected, I spent a lot of time on Day 1 getting my bearings. Rather than figure out double iteration on my first day, I manually walked from elf to elf to compute the final answer.

I hadn't yet learned about some newer features in Dreams, like scope (which was added sometime in the last couple years.) Those features would make subsequent days considerably easier.

Day 2: Rock Paper Scissors

1:30:00 vs. ~15:00
/ /

I put in a tiny amount of visual polish, and I think it paid off. This one is still quite simple, and some changes I made to iteration were an improvement, but I still ran into a lot of issues. (More on that later.)

Day 3: Rucksack Reorganization

4:44:32 vs. ~15:00
/ /

This stream went for a grueling five full hours. I spent about half the time just fixing bugs in my iterator and weird suppressed-behavior / bad-cycle bugs in the larger program. I also spent at least an hour just inputting the example data, which was unusually large. It took me a staggering 4 hours and 45 minutes to get the right answer on this one.

I ended up abandoning the internal timer on the iterator on this day. This was the right decision.

I didn't have the energy to make this one pretty. Not sure how I would have anyway.

Camp Cleanup

Supply Stacks

Tuning Trouble

No Space Left On Device

Treetop Tree House

Rope Bridge

Cathode-Ray Tube

Day 4: Camp Cleanup

35:00 vs. 4:57
/ / /

I opened this stream saying that “my goal is to solve this problem in less than five hours”. Luckily, this one took me literally just a half hour. In fact, it went so fast that I decided to do part 2 as well - which only took another half hour. The iterator worked fine, which was a huge blessing.

This is a fun one to watch if you want to see a speedrun of the process. No other day would be so easy.

Day 5: Supply Stacks

2:08:40 vs. ~15:00
/ / /

I love this problem. It exemplifies everything that is great about Dreams, as well as all the issues I had throughout the challenge. The result is a literal simulation of the problem; the program is also so fragile that it breaks if you breathe on it. All the highs and lows are in one place.

If I could revisit this one, I would probably save a full hour due to better handling of iteration and of large cycles in the graph. I talk about cycle issues later on.

(Skip back a bit in the video if you want to see some shenanigans.)

Day 6: Tuning Trouble

0:56:15 vs. ~15:00
/ / /

I don't even remember this one tbh. It went really fast. I guess it all worked fine right away?

Day 7: No Space Left On Device

3:46:00 vs. 13:02
/ / /

This was the first problem that required me to "allocate memory", that is, spawn objects in the world to store data and run logic of their own. This thankfully went pretty well, in part thanks to my experience two years ago with the LISP thing.

I took what I think are some justified shortcuts in this program - it seems that the input always follows a depth-first traversal of the file data, so I hardcoded a depth-first traversal in my implementation.

Day 8: Treetop Tree House

3:00:00 vs. 8:17
/ / /

This is one of my favorite simulation problems from this year. When the problem described trees occluding other trees, I couldn't help but think "RAYCASTS!!" (or, Laser Scopes, as Dreams calls them). Iteration was working pretty reliably at this point; although this problem took three hours to solve, most of that time was spent learning how to properly use Laser Scopes.

It is just absolutely delightful to me that I can push a little guy around using a Follower and he actually does a run animation. That would never work in any other engine but it saves me so much effort here.

Day 9: Rope Bridge

3:10:00 vs. 12:51
/ / /

This one turned out great as well. If you imagine a rope between the two elves, you can imagine the back elf being tugged around only when the front elf is at least two tiles away.

Amusing fact: I originally had the elves face in the direction they were walking, but disabled it because the code was attached to the elves, and when they turned around, the code would turn around. How many other programmers have ever had their code run away from them?

Day 10: Cathode-Ray Tube

3:00:00 vs. ~5:00
/ / /

I spent SO LONG entering the example data for this one. It is just cruel to give a Dreams programmer sample input that is 146 lines long.

Worse yet, I realized later than the actual input was only 140 lines. So, I spent another half hour entering the actual input, and managed to produce my first and only legitimate Advent of Code answer from Dreams.

At least this one was pretty!

Monkey in the Middle

Hill Climbing Algorithm

Distress Signal

Regolith Reservoir

Beacon Exclusion Zone

Day 11: Monkey in the Middle

3:00:00 vs. ~10:00
/ / /

This problem was really incredibly difficult. I spent at least an hour just figuring out how to make monkeys toss objects to each other, and at least an hour debugging sync issues again. Some days are just like that.

I ran out of time at the end of the stream, so I wasn't quite able to make it as pretty as I would like. Despite that, and despite all the grueling bugs, I think this is one of the most technically impressive simulation problems I completed.

Day 12: Hill Climbing Algorithm

6:00:00
/ / / /

This is, without a doubt, the most technically challenging problem I completed. It took two days and six hours total, and required both recursion and wireless communication between adjacent tiles. It recursively flood-fills the scene by spawning elves to go to each unvisited tile - whoever gets there fastest took the shortest path.

(The elves ascend to heaven because otherwise Dreams yells about having too many entities.)

Day 13: Distress Signal

3:00:00
/ / /

This problem was a welcome reprieve. I'm proud of the solution here; I came up with a way to transform the obvious recursive solution into an iterative one, dramatically reducing the complexity of my Dreams logic. More info in this follow-up tweet.

Day 14: Regolith Reservoir

3:00:00
/ / /

I am so happy with this one. It directly simulates the problem, has great visuals I found on the Dreamiverse, and I even verified that it works with other inputs too (skip to 3:23:39 for an example). Cellular automata turn out to be really fun in Dreams!

There were a couple weird issues, like trigger zones not detecting anything on the first frame they're active. Iteration was still difficult. But the end result is just wonderful.

Day 15: Beacon Exclusion Zone

2:30:00
/ / /

This one looked cool at the end but was quite a grind. Taxicab / Manhattan distance stuff was fun to work with, though. I didn't really bother to actually produce a legit answer for real, but I did get the simulation stuff working.

This would be the final problem I completed; Day 16 was ludicrously difficult, and I had to admit defeat.

A whirlwind tour of logic in Dreams

Before we go further, let me introduce you to programming in Dreams.

Dreams code is made up of nodes and wires, superficially similar to some other visual programming systems. However, it is very tightly designed and elegantly integrated into the game world. In this example, a Trigger Zone widget is wired to the Glow property of the lights on this Christmas tree, causing it to light up whenever the player walks into the zone.

Widgets can be snapped onto a Microchip to make logic more organized. Microchips can have explicit input/output ports, allowing Microchips to serve as reusable units of code, sort of like functions or macros. Being on a Microchip does not affect the widgets’ behavior (outside of inheriting a couple properties like power).

Widgets have their own state, just like any other object in the world. For example, Timers can be turned on and off, Selectors remember which port is active, and Signal Generators just do their own thing.

The only data type is decimal numbers. All wires are just decimal numbers. “Boolean” signals are typically communicated with the number 0 or the number 1.

For convenience, there are several types of “fat wires” that are just common bundles of other wires. (These are basically structs.) All the nodes in Dreams interact with fat wires in pleasant ways, such as the Calculator node working component-wise (allowing for both vector and scalar arithmetic). Each fat wire type can decay to a single number if necessary.

There is no explicit execution flow like you might see in Unreal Blueprints.[2] It’s usually best to think about Dreams logic as independent actors sending signals to each other. In practice, this is often literally true, since logic is usually attached to physical objects in the game world.

Execution is loosely ordered beginning-to-end according to node inputs and outputs; this is obviously necessary or math expressions might take multiple frames to “settle”. However, Dreams doesn’t prevent you from making cycles in your graph, and this can occasionally lead to the “settling” problem I just described. I address this pain point later on.

A story about an iterator

One piece of logic is a perfect microcosm of everything I learned from this challenge: the Iterator.

Every problem required me to iterate through a list in some way. Every problem had some number of lines of input, which I would loop through. My logic for this started off simple and messy, then became complicated and messy, then finally became simple and clean. Let’s go through those steps:

Day 1: Simple and messy

My intuition for any list or sequence in Dreams was to use a Selector. It has up to 10 output ports, and can step from one state to the next. Sounds like what I need! By using the selector to power other nodes, I can use this to step through a list of values. This structure worked well and I used it every day, but the challenge is automatically stepping to the next item.

I originally structured my logic like so: do stuff with the current values, and then send a wire back around to “Move to Next Output” to continue. The problem, though, is that Next Output is never powered off - and when the signal is always high, we don’t keep stepping.

I fixed this on Day 1 by wedging a timer onto Next Output. The logic for resetting this was annoying and fragile, but I got by.

I also had issues where I would step to the next port *before* the current port had finished processing. Dreams doesn’t have a clear notion of execution order, so when you have cycles in the graph, execution doesn’t always start where you want it to start. In this case, I wasn’t reliably processing the first item in the list.

I hacked my way through Day 1, but after sleeping on it, I had some good ideas that would help me in Day 2.

Day 2: Complex and messy

My core insight for Day 2 was to have two distinct phases when iterating: using a value, and stepping to the next value. In a system with unclear execution order, it’s dangerous to process an item and step to the next item on the same frame. It’s not exactly a race condition, but it feels like one.

My idea was to have a clock ticking high, low, high, low on some interval - when it’s high, use the current value, when it’s low, step to the next. I implemented this with a Signal Generator like so.

This worked for Day 2’s relatively simple problem. I would not be so lucky on Day 3.

Day 3: More complex, more messy

Day 3 introduced a new wrinkle: nested iterators.

Suddenly my clock-based scheme started to break down. The outer iterator needed to step only when the inner one finished. Furthermore, I needed to reset the inner one when the outer one stepped. To accommodate this, I pulled the Signal Generator out of the iterator and replaced it with a Next input. When high, step, when low, use. I also made the iterator aware of the total number of items so it could output a Done signal; this was necessary for the inner iterator to tell the outer iterator to step.

This added a lot of complexity, and it took two solid hours of debugging to get the inner iterator to reset correctly. Functionally, though, this was an improvement. The iterator was now more “pure”, a state machine with no internal timers, whose behavior depended entirely on the signals fed into it.

Day 5: Execution order interlude

My iterator would serve me reasonably well for the next several days. The one big exception was when Day 5’s solution was plagued by heisenbugs, and were eventually “fixed” by…removing a single wire.

Basically, I had put myself in cycle hell. As mentioned, Dreams doesn’t have a clear execution order, especially when you have cycles in your graph. As my program got more complicated, and my cycles got larger, my logic became more likely to start execution “in the middle”, yielding seemingly impossible results. At one point, I replaced one wire with a hardcoded value slider of the same value - and all my problems fixed themselves. This led to a rant (see video).

My fix for this was to use Wireless Transmitters to send the Next signal to my iterator. Wireless Transmitters do not cause cycles in the graph, and according to someone in my Twitch chat, always incur a one-frame delay. This is good for me; it allows me to suggest an execution order. I would use this technique every day going forward, and it did reduce the number of cycle-based heisenbugs.

Day 10: Simple and clean

I struggled through several more days with my iterator, and kept running into problems. Resetting in particular was a huge headache - I discovered that my logic would break if I fired Reset while Next was high. The internal “memory cell” that was storing the current iteration value also occasionally heisenbugged, storing nonsensical values. These issues regularly lost me at least an hour a day, and I was getting pretty demoralized.

The Iterator was also so complex that I could hardly understand it. At one point, Eric Wastl (the creator of Advent of Code) remarked in my chat that I needed a logic analyzer, like you would use with real electronics.

Sometime between Day 9 and Day 10, though, I had a horrifying realization: I had basically just reinvented the built-in Counter widget.

My iterator had a Total, a Next, and a Reset. The Counter has a Target Value, a Next, and a Reset. My Iterator output Done when the count reached the Total. The Counter outputs Done when the count reaches the Target Value. The only thing the Counter didn’t have was the Use/Next split.

On Day 10, I started with a Counter and added a tiny amount of wrapper logic. In minutes, I had perfectly recreated my old Iterator with none of the bugs:

I made zero modifications to this iterator for the rest of the challenge. It was perfect. And I was so annoyed with myself.

How did this happen?

I had drifted toward the Counter design so slowly that I didn’t realize it. I started with a selector and no explicit counter, then transitioned to a clock, then added the inner counter, and then removed the clock. The Counter, though, didn’t have the Use/Next split, which is why I didn’t think to use it earlier.

In the end, there were two good ideas that I had to discover:

  • Separate Use and Next into two mutually exclusive states to avoid same-frame bugs.
  • Drive the logic manually instead of using an internal clock. Timers belong at the highest level of your logic, never hidden inside other widgets.

Both of these ideas apply broadly to programming in Dreams, so let’s break them down.

Lessons learned

Lesson 1: Make logic mutually exclusive

Tons of bugs boiled down to two nodes being active at the same time, when they shouldn’t have been. Early on, I tried to fix this ad-hoc with lots of AND gates powering specific nodes. Later on, though, I realized that grouping logic into Microchips not only helped me organize my logic, but actually fixed bugs too.

Putting logic in Microchips allows you to power entire chunks of your program on and off. Instead of running logic and then randomly suppressing some results, you can simply not run the logic. I think this actually fixes a lot of heisenbugs by ensuring that all the wires are zero on the first frame each widget is active. The more I used power in this way, the more I liked my logic and the fewer bugs I encountered. I developed a mantra: “don’t use an AND gate, use power instead”.

This way of thinking is probably old news to those more experienced with Dreams, and here’s why: the Timeline node, which is used for all animation, sound, and fancy sequencing in Dreams, literally just powers nodes on and off. Someone on Mastodon remarked to me that “timelines = scripts”, and it blew my mind.[3]

I’ve never done animation. I’ve never done sound design. I’ve really never tried to make a game in Dreams; that’s not my thing. But this led to an enormous blind spot, because I never used Timelines, and therefore never understood how important power is in Dreams. Instead of chaining Selectors to get lists of more than 10 items, I could have used Timelines. Instead of using mazes of logic gates to power things on and off, I could have used Timelines (with multiple tracks). Instead of awkwardly sequencing things with Selectors and Timers, I could have used Timelines. The list goes on.

In the end, what matters is making major chunks of logic mutually exclusive. Whether you use Timelines or power things manually, you will benefit from isolating them in this way. Your logic will be easier to follow and you will avoid heisenbugs. It takes practice, but Dreams is well-designed, so well-structured logic actually does feel better. You’ll know it when you see it.

Lesson 2: Use timers, but keep them out of core logic

One of the most influential programming talks I’ve seen is Gary Bernhardt’s Boundaries, in which he advocates for programs to be structured with a “functional core” and “imperative shell”. Keep your program’s core logic pure and functional, free of side effects, and push the messy (but necessary) imperative logic to the outside. Keep the core code free of side effects so it’s trustworthy and predictable, and your leaf code can be as messy as it wants to be.

In retrospect, that advice applies to Dreams just as much as any other programming environment. A Dreams project needs core logic and gameplay systems, but also needs delays and animations and time for physics objects to settle.

At first, I wrote my logic without timers. But this led to same-frame execution issues, and it was difficult to visualize my logic in the world. Then I put timers all over the place, but every time I put a timer inside some “reusable” logic, it would later blow up on me. My iterator is the perfect example; first it did all the work at once (and was buggy), then I gave it an internal clock (which was buggy), and eventually it was a pure state machine (that worked reliably).

“Purity” in Dreams logic isn’t exactly the same as “pure functions” in programming; I don’t know if functional programmers would consider any state machine to be “pure”. But Dreams is, at its heart, a bunch of state machines talking to each other[4]. If you want something to be reusable, keep side effects out of it.

I’ve described Dreams before as “the best object-oriented programming system I’ve ever used”, and this is why. I’m not claiming that Alan Kay’s vision is the vision for OOP, but I have to say, Dreams is pretty close to Alan Kay’s pseudo-biological, simulational description of OOP. It’s the “actor model” writ large. It’s a concurrent, stateful, message-passing system.

Final thoughts

I’m really glad I did this challenge. My overall impressions of Dreams are still very positive. I’m genuinely delighted by how easy it is to kit-bash beautiful scenes together. I’m amazed by the quality and variety of material on the Dreamiverse. And I still think the logic system is brilliant.

But obviously I struggled to do things that would be simple in a “real” programming environment. I put together a spreadsheet tracking my times in Dreams vs. my times in JavaScript[5], and I was generally about 15x faster in JS. Even if you account for the time spent just thinking about the problem, and the time spent fixing bugs instead of “really programming”, this is still a staggering difference.

I just chose JS because I know it well and it was on my computer. I think it's pretty representative of "normal programming" these days, both in terms of general language capability and execution speed.

So let's break down what I liked, what I disliked, and what I think Dreams is good at.

What I like: Programming in Dreams is very tangible. And not because the code sprawls across my screen, but because it’s associated with the data it manipulates in a very real way. It’s embodied. There is really no boundary between the code and the world, and this allows for some wonderful workflows. For example, there is no boundary between programming and debugging - if something is acting weird, you can simply pause time, open the logic, and probe widgets and wires to see what state they’re in. Tweak the logic on the fly and resume again. It’s blissfully iterative and doesn’t require you to learn any additional tools, e.g. a time-travel debugger or a Whitebox. This is exactly how scripting should be.

Plus, every feature of Dreams is just so tightly designed. It’s one of the most thoughtful systems I’ve ever used. The logic system can be simple because the rest of the engine is so carefully designed. It’s an incredibly coherent experience.

What I dislike: Like I said, I'm horribly unproductive compared to traditional programming, and it's hard to imagine ever closing a 15x productivity gap. The logic system is great for orchestrating high-level game behaviors but bad for building systems. Advanced Dreams programming feels like working with electronics - instead of high-level programming constructs, I've got a bunch of wires, transistors, and an oscilloscope. (I mean, units of logic are literally called Microchips.) This is not the level of abstraction I like to work at.

On the other hand, I was able to go from a working implementation to a great visualization in minutes. The connection between Dreams logic and Dreams objects is so seamless that it’s trivial to visualize your program in pleasant ways. This part of Dreams works extremely well.

So in an ideal world, I think I would like to write my own nodes. Instead of making my state machines with faux electronics, I’d like to actually write them in a more powerful language. Then I could tie them together with Dreams’s lovely scripting system and game engine. I legitimately think that this could be an incredibly powerful way to program.

What is Dreams good at? Simulation. All the best programs were the ones that could take advantage of the 3D, physical world the code lives in. The ones that work more abstractly with data, e.g. navigating a filesystem or solving optimization problems, are hard to represent in Dreams at all. It's possible that things would be different if the system somehow supported lists, and recursion, and more advanced forms of data, but Dreams is fundamentally built for simulation, not number-crunching.

Do I recommend Dreams? Absolutely yes! It's just so much fun. I did this challenge because I wanted to find the boundaries of Dreams's logic system. I think I found those boundaries. I certainly won't boot up my PlayStation next time I need to solve a dynamic programming problem. But I have come away really inspired about scripting, and concurrent programming, and the things you can achieve with a tightly-integrated system. This is where Dreams shines, and I cannot recommend it enough.

Thanks!

Obviously a huge thanks to Media Molecule for dedicating the last 15 years to this weird, wonderful system. It’s a miracle that Dreams exists at all, and it’s all because of the LittleBigPlanet series, the studio’s technical prowess, and their incredible dedication to user-created content.

Thanks to various members of the Dreams community for hanging out in my stream, and a particular thanks to TAPgiles for his Dreams documentation and tutorials.

Thanks to the Handmade community for the years of discussion about visual programming, in particular d7 for his imagination and relentless dissatisfaction with the status quo.

And thank you for reading! 🙂