Implementation notes Abuse the scripting languages. ---------------------------------------------------------------------- 0. Concept A game that is playable by attaching more interpretors to the pipe. Player's actions are indicated by which interpretor they choose. I wanted to write this program just because polyglots are cool, and I don't think chained polyglots are particularly common. It's also because I thought "Hyperdimension Neptunia" was such a great series that I thought I should make some ASCII art for it, hence the title and format "Neptune". Though actually, my hidden agenda is to illustrate the over proliferation of scripting languages in an ironic way. Sure, you never have to compile anything, just add more wrapper scripts! But when there are 100+ layers of wrapper scripts, you might start to wonder whether a language that compiles to native code might have been a better choice. Neptune consciously forces users to end up in these absurd 100+ wrapper situations. ---------------------------------------------------------------------- 1. Polyglot Polyglots generally aren't that difficult to write, the key is knowing how each language quotes strings and comments, and the rest is just a bit of trial and error. For Neptune, I have decided that at least 4 languages are needed to make interesting games, so that players have 4 choices to make at each step. I have considered what interesting games would be possible with only 2 changes, and it seems that the most interesting game would have been Russian Roulette, and I wanted something more cheerful. So 4 languages it is. I have also decided on implementing a maze game, since maze generators can be implemented in relatively small amount of code, and do not require too much patience for players to win (see section 4 for more on this patience metric). Having decided on a count of 4, I need to decide on which 4 languages they would be. In the end it was perl+python+bash+ruby, which seem to be the 4 most commonly installed scripting languages. All these languages provide multiple ways to quote strings, these were my choices: perl: q?string? python: '''string''' bash: 'string' ruby: %{string} Some way to ignore large portions of the file would be convenient. There is __DATA__ for perl, and bash doesn't care about anything after it has exited. So we can get a 4 language polyglot like this (see poly.pl): q='''=; print "Perl\n"; __DATA__ '+%{ echo "Bash" exit 0 ''' print "Python" ''' } print "Ruby\n" #''' Getting this far didn't take particularly long, other than the fact that I have never coded in ruby before. Fortunately it was pretty easy. ---------------------------------------------------------------------- 2. Polyglot pipe Having figured out how polyglots would work, next task is to figure out how to make them act as different filters. I thought it would be best to have the majority of the maze logic implemented in a single language, and just have all other languages shell out to that language. This avoids duplicating code. This is the structure I settled on: perl = perl all other languages = set variable + feed input to perl via pipe In other words, for every action that didn't involve perl, the user would create twice as many pipes :) The individual segments are structured like this: quote perl code __DATA__ bash code to feed quoted code to perl exit 0 python code to feed quoted code to perl ''' ruby code to feed quoted code to perl ''' lots of blank lines maze data (user readable text) The quoted perl code is compressed and uuencoded to save space. Maze data is used to only display current game status to the user, the maze is actually regenerated at every step. ---------------------------------------------------------------------- 3. Polyglot pipe entry After deciding how each output polyglot program would look like, we are left with the easy task of writing the first program that would output these polyglots. It's easy because this first program will not be a polyglot (thought about this at length, decided that a 4 language polyglot ASCII art is too much effort for now). At this point, it was just the usual process of reducing code to a reasonable size, and writing a generator to fit the output to a template. ---------------------------------------------------------------------- 4. Maze game Besides the polyglot stuff, the maze game itself is fairly straightforward: do a depth-first-search of all neighbor nodes in random order, repeat until all nodes have been visited. There are some config tweaks that are less obvious: - Each maze cell takes up 2 characters, plus extra character for walls, so each maze cell takes up 3x2 characters of screen space. I thought about single character cells (like nethack), but I thought making the maze logically *smaller* would make it more playable, hence the larger cells. - 26 columns and 12 rows would fit in a 80x25 terminal, but the maze are generated with only 25 columns and 8 rows. Again, going for smaller maze size. In particular, the 8 row limit is to avoid scrolling the top of the maze off of the terminal due to the extra long command lines (it's not really avoidable for large mazes, but it helps). - Middle part of the maze is reserved for instructional text. Partly because this helps first time players in knowing which direction maps to which language, and also to reduce the maze size by 14 cells. - I thought about randomizing the directions so that players are required to look at the instructions to figure out which direction is which, but decided that it will be less frustrating if the directions were fixed. Actually, I made perl the "right" direction, because most mazes require going right more often than any other direction, and using perl for that direction would reduce the number of processes spawned in the pipe (every other direction costs an extra process). - Neptune has her own random number generator built in for portability. Also for portability to 32bit platforms (such as the C++ solver), this generator uses only 31 bits of state. This ensures that the same seed number results in the same maze across different platforms. Note that most of these tweaks are for reducing the maze size. Because, as predicted, having pipes this long causes the player to wait a few seconds at each step to move around in the maze, and I doubt most people would have much patience to play if the maze was any larger. I wanted the maze to be small enough so that people would play them manually, but not so small that the maze would look boring (except maze #0, which is intentionally boring). For people with enough patience to solve the maze (or just copy&pasted one of the existing solutions), they are rewarded with another ASCII art at the end. I thought about using Purple Heart for this final image instead of Neptune again, but it's hard to tell the two apart at 80x25 resolution, so in the end it was just another smiling Neptune. In theory the longest maze would be one with 186 steps in the path, visiting every possible cell in the maze. In practice, the longest maze has a path with 172 steps, found after solving all 2^31 mazes by brute force. Only maze #1136163692 requires 172 steps to solve, all other mazes can be solved in 170 or fewer steps. I used the longest maze as the example maze in the main documentation, actually I wrote the C++ solver specifically to find this longest maze. It fits my plan on illustrating this over proliferation of wrapper scripts. ---------------------------------------------------------------------- 5. Finally... After all is implemented, the last step is to test and document everything (seasoned engineers might argue that this should have happened in reverse order, but they should also observe that this is often the order that happened). On the testing front, Neptune has been verified to work with these combinations of interpretors: Linux Linux Linux Cygwin Perl 5.8.8 5.10.1 5.14.2 5.14.4 Ruby 1.8.7 1.8.7 1.8.7 1.9.3p484 Python 2.5.2 2.6.6 2.7.3 2.7.5 Bash 3.2.13 4.1.5 4.2.25 4.1.11 On the documentation front, Neptune has been honestly straightforward to write, so not much to document :) But I did want to document this hidden agenda bit. Arguably it's no longer hidden agenda since it's documented here, but no worries, nobody reads documentation anyways.