3

I did Advent of Code on a PlayStation

 1 year ago
source link: https://bvisness.me/advent-of-dreams/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

I did Advent of Code on a PlayStation

December 31, 2022

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?

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!

day1.jpg

Calorie Counting

day2.jpg

Rock Paper Scissors

day3.jpg

Rucksack Reorganization

day4.jpg
star4.png

Camp Cleanup

day5.jpg
star5.png

Supply Stacks

day6.jpg

Tuning Trouble

day7.jpg

No Space Left On Device

day8.jpg
star8.png

Treetop Tree House

day9.jpg
star9.png

Rope Bridge

day10.jpg

Cathode-Ray Tube

day11.jpg

Monkey in the Middle

day12.jpg
star12.png

Hill Climbing Algorithm

day13.jpg

Distress Signal

day14.jpg
star14.png

Regolith Reservoir

day15.jpg

Beacon Exclusion Zone

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.

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]

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.

calendar.jpg

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.

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.

imp.png

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! 🙂


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK