asm-blox: a game based on WebAssembly that no one asked for
Zachary Romero (zacromero@posteo.net)
In this talk, Zachary Romero shares a game he wrote and how he made it. Afterwards, he will handle questions over BigBlueButton.
Talk
00:00.000 Introduction 00:30.680 TIS-100 00:44.960 WebAssembly 01:08.040 Basic stack operations 02:07.640 Numeric commands 02:44.680 Boolean operations 03:21.400 Port operations 04:00.240 Control flow 05:15.720 Modules 06:14.480 Puzzle 08:33.040 The game loop 09:35.200 Tic-tac-toe 11:25.880 Text properties 12:07.800 Code cells 14:00.920 Undo 14:37.560 Parentheses 14:52.360 Assembly text to executable code
Q&A
00:00.000 Introduction 01:12.600 Why did you choose an internal state versus many 'state buffers'? 02:10.720 Do you have plans to port shenzhen.io to Emacs? 02:29.960 Did this use WASM? 02:59.800 Why wasm rather than a more traditional Assembly dialect? It wouldn't be harder to implement, right? 05:08.960 Any next projects on your mind? 05:52.680 Does this work with any other paren-based editing packages? 06:46.920 What kind of tool could use this idea? 07:56.280 How did you go about designing the puzzles? 08:39.320 What are your favorite changes in the upcoming Emacs 29? 09:07.480 Are there tools to add more puzzles?
Description
Over the past decade, programming games have risen in popularity and become a genre unto themselves. They are loved for their open-endedness and have helped people get into programming as well as help programmers hone their problem-solving skills. As a fan of the genre, I decided I wanted to recreate such an experience in Emacs. Looking at the already existing collection of games, TIS-100 by Zachtronics stood out as an especially good candidate for the base of a game, where the user is entering assembly code into a terminal to solve puzzles. The game asm-blox switches things around and instead of programming register machines, you program mini stack machines in a language similar to the WebAssembly text format.
I'm still wondering if the game is actually any fun or not but either way it was an interesting project to make. In this talk, I'll demo the game as well as go over some of the Emacs Lisp tricks I used to make it work.
The source code can be found at https://github.com/zkry/asm-blox
Discussion
Questions and answers
- Q: Why did you choose an internal state versus many
state buffers
? (ie. actual windows) Thanks!- A: A single internal state is easier to deal with in the context of the game. Windows would obviously be better for other normal applications to allow users to customize how they should behave.
- Q:Do you have plans to port shenzhen io to emacs?
- A:That would be cool, was also thinking about exopunks.
- Q:Did this use wasm ? We call some wasm code from Emacs?
- A:No, more similar to TIS-100, just a game.
- Q: Why wasm rather than a more traditional Assembly dialect? It
wouldn't be harder to implement, right?
- A: It would have been easier, but less of a challenge and resemble TIS-100 too much.
- Q:Any next projects on your mind?
- A: Yes, a couple, hopefully more useful. I think tree-sitter is cool. There's a neovim plugin called neogen that generates documentation. Hopefully next year I'll be presenting something more useful.
- Q: Does this work with any other paren-based editing packages?
- A: Not at all (etc. tbd)
- Q: What kind of tool could use this idea?
- A: So I think some sort of graph drawing tool in Emacs might have a similar idea. Like artist-mode but with graph drawing constructs.
- Q: How did you go about designing the puzzles?
- A: With open-ended puzzles like this, coming up with random ideas that seem like they should be implementable usually works. If you've seen some of Zachtronics games, the bar is extremely high for what is capable.
- Q: What' are your favorite changes in the upcoming emacs 29?
- A: Definitely tree sitter. I've played around with it and it provides a nice interface for extracting syntax information. Like I can probably rewrite this plugin without any crazy regexs: https://github.com/zkry/go-ttest.el
- Q: Are there tools to add more puzzles?
- A: So the game code itself has a asm-blox-puzzles.el file which defines each puzzle. It's pretty easy to add new puzzles but it involves digging into the code.
- QLike a binding to graphviz? (assume this is a continuation of the
"what kind of tool" question)
- A: I was thinking more ASCII, like a tool I saw called diagon. Like artist mode but for graphs. But graphviz is amazing and a lot could be done with that.
the diagon tool: https://arthursonzogni.com/Diagon/#Math
Other feedback from IRC:
- Great presentation. Game actually looks quite fun
- Thanks again for your answers. Du bist das Beste
Transcript
[00:00:00.000] Hi, I'm Zach and today I'll be giving a presentation on asm-blox, a programming game inspired by WebAssembly. So programming games came into prominence about a decade ago and are loved for providing interesting programming challenges without all the messiness of real world programming. I wanted to make a programming game and I decided to base it off of TIS-100, having a pretty basic UI. It seemed pretty doable in Emacs.
[00:00:30.680] TIS 100 is a programming game where you write a fictional assembly language into a grid of cells which can each communicate with one another, you're tasked with solving fairly simple CS 101 like problems.
[00:00:44.960] To mix things up a bit I decided to base the language of asm-blox off of WebAssembly, which is stack based, as opposed to TIS-100 which is registered based. Here you can see the same program written in the game TIS-100, what it looks like in asm-blox, and the original WebAssembly that it's based off of.
[00:01:08.040] With that said, let's get into a demo. This is the game board. It's a 4 by 3 grid. Each cell has a stack of size 4. First off, I'll show some of the stack editing commands. We can add a value with the const function. Here we're adding two values to this stack to get added, and eventually the stack gets overflowed. We can fix that as follows with the clear command, so that clears the stack. We can duplicate values on the stack. This duplicates the item at the bottom of the stack. 10 gets put on, 20 gets put on, then 10 will get duplicated and put on the top of the stack. We can increment. For example, this increments the second to bottom, the second to bottom from the stack. So 10, 20, increment that, clear. That's basic stack operations.
[00:02:07.640] Next up, we have numeric commands. For example, here, if we add "add", it pops two values off the stack, adds them, and pushes the result on. Another way we can write this is as follows. We can have the add here and then nest the two constants, and then this does the same thing. First, the inner constant operations run, and then the outer add operation runs. We can nest as deeply as we want. There's also subtraction, multiplication, and whatnot.
[00:02:44.680] Next up are Boolean operations. Zero counts as true. Anything else--sorry, zero counts as false. Anything else is true. For example, this would give us false and true, so that result should be false. Zero gets put on the stack, one gets put on, and then the "and" operation. So there's also or, not, and various numerical comparison operations like greater than and less than.
[00:03:21.400] Next up are the port operations. We can send values to other cells as follows. Here we create a value and then send it right. Let's run this. The 10 goes on the stack, and then it gets sent to the right. Here it's waiting for this cell to pick it up. It can pick it up just as follows. So left... and then why don't we have it drop that value after it gets it. So the 10 gets sent to the right. This one picks it up and drops it.
[00:04:00.240] Lastly, we have control flow, which is a bit tricky, but with this visual, it helps explain it. There are two block constructs, "block" and "loop", and there's two jumping constructs, "br" and "brif". So if "loop" is jumped to, the control flow goes to the beginning, the top of the loop. If a block is jumped to, it goes to the end of the block, and these various blocks are identified by their level of nestedness. From the point of view of this jump statement, this "br" statement, this is block level 0, this is 1, this is 2. So here, "br 1" would be referring to this loop. What this [br 1] would do is, it would jump to this loop right here. If we were to do this [br 2], what this would do is, this would jump past this block right here. So as another example, this right here, this is a loop that generates increasing numbers.
[00:05:15.720] Let's see. Next up, we have modules. This is an example of a stack module. In addition to stack, there's also heaps. What this does is it allows us to create an extra stack that we can push and pop items onto. This one can have as large size as we need. Here it has a size of 20. It's taking values from up and exposing those values on the left. This loop right here, it generates numbers, and it's putting them onto the stack. We can see here that those numbers are being exposed to this cell right here. It's just taking values, and eventually, it's going to overflow and cause an error. That finishes the basic commands.
[00:06:14.480] Why don't we try solving this puzzle. The puzzle description is right here. We want to read a value from I. Send 1 to G if I is greater than 0. Send 1 to E if it's equal to 0. Send 1 to L if it's less than 0. And then all the other ones, we send 0 to. First things first, let's send the value we get from the input down as follows. Let's send that value right. You get from up. Okay. So next, we're getting a value on the left. Now we want to compare if this number is greater than 0. If it's greater than 0, we send 1 to G. Let's perform the greater than operation on that item we just got, and we're comparing it to 0. Now that result, we're going to send down, and we're going to send this original value we got from here to the right. Here, we do a similar step. We get the value from the left, but this time, we have to do an equal operation. Is that number we got equal to 0? We send that result down, and then send this number to the right. Lastly, we get this number from the left. Here, we need to compare if it's less than 0. We send that result down, and now lastly, we drop that remaining value. Okay, let's--oh, and then lastly, we need to send down the value we get up. Send down, up, send down, up. Okay, so let's try running this. Let's see. We notice that the numbers are coming in from I. They're going through our various conditions and should be sending all the correct values. It looks like we're not getting any errors so far. Let's speed this up. That completes the puzzle.
[00:08:33.040] Now let's get into some of the implementation details. The first thing is the game loop. The game loop is... So this is actually extremely simple. All the state for the entire game is stored in just a few variables. There's one variable storing the text of each cell as a vector of strings. There's a single function that renders the entire game, the entire board. There's a single function that would render this entire screen based off of the state, and then the game waits for you to press a key. The key usually, depending on what action you perform, updates the state and causes a re-render. It's an extremely simple game loop, but it makes implementing it pretty easy. To demonstrate how this game loop works,
[00:09:35.200] I have a simple demo prepared. This is a game of tic-tac-toe. Let me show this real fast. It's an extremely simple implementation, but it follows the same principles that I used in asm-blox. First, we have the state defined in variables. Here we have two pieces of state. We have which player's turn it is and the state of the game board. The player turn can be nil if it's empty, the string "x" or the string "o". Then the game board is a list of nine board elements. So that's the state. Then we have a helper function. You can go into the details, but it just returns true if the board has a winning player. Part two is the rendering function. Only based off of the game state, we have a function that erases the buffer and draws this from scratch. That's this part right here. Lastly, we have the action. We have one action which is bound to RET, and it places a player token. Once it places a player token, it rerenders the board, and all the rerendering is handled by this function. Then we have just creating of the mode and initialization function. With these three steps it clearly separates out all of the state, the rendering, and the actions, and it makes implementing it very simple.
[00:11:25.880] One trick that's used here and that I use in my asm-blox game is that when I render the board, I propertize the text to contain extra information. For example, here, each cell has a tic-tac-toe index to indicate which number cell it is. This has index 0, 1, 2, all the way up to 8. That way, for placing, the only thing it has to do is just look at its position based off of the text property. It makes implementation extremely simple.
[00:12:07.800] Next up, we have the implementation of the code cells. If you notice, here it's kind of weird how it's like a buffer, but each cell kind of acts like its own buffer, and it has its own limits. All of the Emacs editing-- well, some of the Emacs editing commands kind of work, like beginning-of-line, end-of-line, end-of-buffer. How is that done? Well, it's all just a trick, actually. Each cell has text properties of which line it's at and its cell coordinates. Whenever a key is pressed for editing, moving lines-- there's even kind of more complicated things like switching cells around-- so all of that, it knows which position it's in, it knows what cell it's in, and then it copies the text of the cell, because remember, the contents of the cell are stored in internal state. It copies that cell contents into a temporary buffer. It then moves the point to whichever line it was in the game board. It performs the action. It makes sure that the resulting text isn't longer than the cell width or the cell height. If everything checks out, it updates the state and calls a re-render. So there's nothing going on in here that's, like, actually inserting a letter A. It's all updating the state and causing a re-render.
[00:14:00.920] So this makes things like certain internal Emacs editing constructs pretty hard to use, like undoing. Normally the undoing construct works off the contents of the buffer. But if your buffer is actually just a reflection of the internal state, then how does undoing work? Well, it pretty much is kind of a hack. I mean, undoing is here, but it's pretty much redone in a not so configurable, not so modifiable way.
[00:14:37.560] Pretty much everything is like that, from these parentheses highlighting... Normally, parentheses highlighting would be kind of weird, with cross-line parentheses and everything. All of that had to be redone.
[00:14:52.360] Another point about how this is implemented is the assembly text to executable code. If you're familiar with WebAssembly you might have encountered a tool wat-wasm. It basically converts the WebAssembly text format to byte code. And what I do here... It goes through a similar process. Normally, when you're writing this text format, you can nest things as deeply as you want. Basically, what happens is it flattens out everything. It kind of knows the order that all these things are going to get executed, and then it puts it into one single line that it can just run through and execute. The same thing for the loops and blocks. It internally generates labels and jump statements. So that concludes this presentation. Thank you for listening, and I hope you enjoy the rest of the conference.
Captioner: sachac
Questions or comments? Please e-mail zacromero@posteo.net