quinesnake: a quine that plays snake over its own source!
A quine is a program that takes no input and prints its own source as its only output. “Stepping outside itself”, e.g. by printing the contents of its own file, isn’t allowed; so you can't just write a program which reads its own source file and prints it to stdout. Quines are generally interesting to look at because if you haven't written one before, it will likely seem extremely difficult or impossible to write.
Source as data
The standard trick used to write a quine is to represent a copy of the program’s own source as data within the program, usually in a string. It can then be formatted into itself and printed. This two-line Python quine is a neat example of xthis:
s = 's = %r\nprint(s%%s)'
print(s%s)
Quinesnake uses this string formatting to allow it to print its own source, but makes things more interesting by playing the classic game snake over the source (with wasd controls) after it’s printed! It’s still a quine, it just runs a game loop to accept keyboard control input, and highlights parts of the text as it continuously prints it to render the snake and the food, using the curses library:
Starting out
Finding a good place to start writing a program like this is quite tricky; do you start with the quine or the
game?
In my case, since I didn't know quite what the completed code would look like when I started, I decided to
start by
representing the game board as an array of #
characters:
std::wstring s = L"####################"
"####################"
"####################"
"####################"
"####################"
"####################"
"####################"
"####################"
"####################"
"####################";
The reason why std::wstring
is used here instead of std::string
is explained later.
The commented version of quinesnake uses a board like this to play snake over. Once the game was
functional I
replaced the #
characters with the source of the program (the source as data). The logic of
the snake game itself
is quite trivial and is available in a readable format in
quinesnake-commented.cpp.
Minifying the code
The game playing code needs to be as small as possible since it'll be represented twice in the source. There
are a
number of techniques used to make quinesnake as small as possible. Perhaps the most interesting one is that it
compiles itself. Making the source file executable and executing it invokes g++
on itself with a
number of flags,
includes and defines. This works because the first line of the program, /*bin/ls>/dev/null...
, is
interpreted as a
shell command, despite also being a valid C++ comment. The sed magic, borrowed mostly verbatim from
stedolan’s minhttp project, parses the C++ comments (which
contain the shell
commands to compile the program) out of the source file and executes them as a single shell command. Without this
it’s really tricky to minify the program, because include and define statements must be on their own line.
To keep track of the state of the game, and to make the source as confusing to read as possible (which is also
important!), quinesnake stores the game state in the spare bits of the source-as-data string used by the quine.
The
type of this string is std::wstring
,
a string class for storing
wide (>=16 bit, or wchar_t
) characters. Since quinesnake only uses it to store 8-bit ASCII
characters, that leaves
the rest spare to store the state of the character in the output (empty, snake, or food. 2 bits needed), and the
direction
of the snake pixel that proceeds this one (up, down, left or right. Also 2 bits needed), if it’s part of the
snake. Every
other wchar_t
in the string contains these 4 bits of game state in the higher order bits
above the regular character,
so they’re removed by casting the wchar_t
down to char when printing.
So the byte format of every other wchar_t
in the std::wstring
is as follows:
+--------------------------------+--------+--------+----------------+
| 8 bits | 2 bits | 2 bits | 4-36 bits |
+--------------------------------+--------+--------+----------------+
^ ^ ^ ^
8-bit ASCII chracter state direction unused
I say every other wchar_t
because there is no reason to do this for every
wchar_t
. The snake is rendered two
characters wide (so that it appears the same width whether it's vertically or horizontally oriented). So
annotating each
position would be redundant.
Finally, string formatting in these programs can be a huge pain when special characters need to be escaped (e.g.
quotes
or newlines) so that they appear as special characters in the source, but escaped characters in the source-as-data
string. To not have to worry about this, quinesnake uses a raw wide string for the data, which does no
special character
escaping. The little substitution that’s required is done manually instead, by manually finding the
%d
substitution token
in the raw string to replace it with the string itself, using string::replace
(although this is only done in quinesnake.cpp,
not in the commented version).
You can see the full source on the quinesnake github repo!