Pong with Cel7
In the last days the handmade network community hosted a lisp programming jam, the resulting projects were really cool. The one that struck me most was cel7, done by rxi, a minimal game framework, to make simple grid games with fe which is a lisp styled language also by rxi.
While the said points may be interesting the part which made me look at it more was that the binary of this framework for linux was only 18kb! This led me to investigate, the first thing I noticed by running the ldd command was that the binary was a static executable. Wut?! How can it be static, interface with X11, have a scripting language embedded and still be only 18kb?
$ ldd cel7 not a dynamic executable
The first step was trying to disassemble the binary and watch the assembly instructions. I did this by running objdump, but to my amazement no instructions were displayed. Facing this I tried another way to obtain information from the binary, I used the strings command. By watching the strings I found that the binary was compressed with the upx packer.
I downloaded the upx packer tool and uncompressed the cel7 binary. Looking at the uncompressed binary now it had 39kb. UPX packer made a big difference but 39kb is still super small. Next I run the ldd command in the binary to see if anything has changed. Now it showed a dynamically linked executable. This solves the mystery of the static binary with such a little size.
$ upx -d cel7 $ ldd cel7 linux-vdso.so.1 libX11.so.6 => /usr/lib/x86_64/libX11.so.6 libc.so.6 => /lib/x86_64/libc.so.6 libxcb.so.1 => /usr/lib/x86_64/libxcb.so.1 libdl.so.2 => /lib/x86_64/libdl.so.2 /lib64/ld-linux-x86-64.so.2 libXau.so.6 => /usr/lib/x86_64/libXau.so.6 libXdmcp.so.6 => /usr/lib/x86_64/libXdmcp.so.6 libbsd.so.0 => /usr/lib/x86_64/libbsd.so.0 librt.so.1 => /lib/x86_64/librt.so.1 libpthread.so.0 => /lib/x86_64/libpthread.so.0
Next step was looking at what was used to understand better how the size for this could be only the 39kb. By checking the list of dynamic libraries I saw that it links against libX11 and libxcb and no other graphical libraries which means it uses X11 directly to build the window. This must be a simple small implementation to keep the size low.
Also the programming language for the framework is compiled into the executable, so it must be really simple or would fatten the binary. After looking at the fe source I found it had less than 900 lines of C code while providing a simple lisp, this explains how it doesn't bloat the size.
After the initial exploration I wanted to try to build something with a framework this size to see how it would behave. Usually when I'm testing a new engine, framework, language, etc. I like to build a pong. I got into this habit because of a video of a guy building a pong in 5min and 30s. So it is my fast initial and simple test. But, in my case, it always takes some more time than him.
The main pong mechanics can be summed up to:
- A paddle controlled with the up and down keys for the player.
- A ball moving around.
- A computer paddle just trying to go in the ball direction.
- Collision detection with the up and down walls, that inverts the y ball speed on collision.
- Collision detection with the paddles, inverting the x speed on collision.
- Checking if the ball went out of screen to any of the sides, giving a point to the player on the other side.
While the mechanics are simple, it took me some extra time to understand how the language worked. The operations are all done with a prefix notation, example.
All the code is S-Expressions which follow the lisp motto of code is data, or said in another way, the representation of the code is the same as the compiler representation. This together with macros allows for some really cool approaches that permits us to change the language to what we want. A simple thing I did was to create an operator to index elements in a list since the language didn't had it.
(+ 5 3 6 (* 2 7 9))
( 2 mylist)
After this I started looking on how the framework worked. The example I followed more was the snake one. The first thing that I noticed was that the whole game sprites were just 7x7 groups of bits associated with characters. The colors were comprised to 4bit, and the operations inserted directly ascii characters to memory or to screen positions. Example of the ball sprite.
0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0
Implementing the game logic was quite simple, most of the time was just understanding how to use the language or the framework. Nothing was that complicated, it was just different from what I'm used to. Also I tried to create some macros to build switch statements and other things to change the language, it was fun but with that the implementation time went up.
After having the base game working I wanted to make it have extra feedback and be more appealing, so I made the ball make a little effect when it hits the paddles. I made the score flash when a player wins some point. Picked the crosshair and apple pickup effects from the snake example and changed them. The crosshair was changed to be permanently pointing at the ball instead of flashing. The apple pick effect I made some more changes, reduced the range of it to make it look like some kind of shorter waves and made it change color between a limited color range.
With this the game was fully working and somewhat pretty. But after playing some time I started finding it a bit boring. To create a constant challenge I made some little changes in the core mechanics of the game. I made the ball speed up 5% every time it hits a paddle, resetting only when it goes out of the window and the game restarts.
This made the game funnier but after getting used to the speed up, the bot couldn't hold up. So another change was done, when the player makes a point the bot gains also a bit of speed. This turned out to be really cool and hardcore, reaching 15 points against this bot is a mad man challenge! I was happy with this game now, so I started preparing the release.
At the moment of the release I noticed another really interesting thing about the framework. I could just append the code to the binary and it would run standalone.
$ cat cel7 pong.c7 > pong
At the beginning I searched and didn't understand how this would work. But some days after rxi explained the technique and it is quite simple. He said:
- When the application is built a null byte (0x00) is written to the end of the executable.
- When the application is run it opens its own executable and scans for the last null byte in the file. If there is anything after the last null byte it's assumed to be a program and is read in, otherwise the files listed by the commandline arguments are read.
- This exact approach only works with text as we're assured the file we're appending to the executable won't contain a null byte of its own.
This also makes another fun thing happen, if you open the executable in a text editor you are able to see the source code after the binary content.
If you want to try the game you can download the binaries here:
If you want to check the code and play with it you can check the following repository.