Retro Coding Fun: Games on your Printer
Nicolas Seriot, 11th April 2025
Slides in PDF format: 20250411_postscript.pdf
Full talk on YouTube (in French)
Discover PostScript as more than just a page description language.
From Tic-Tac-Toe to Tetris, I'll show how a stack-based language designed in 1985 for document rendering can be used interactively.
You'll learn how to transform your printer into a gaming platform through clever hacks and unconventional thinking.
This talk was originally submitted to DevDays Lithuania in May 2025.
My friend Raphaƫl has offered me the opportunity to present it at Renens Fixme hackerspace beforehand.
So, what do your know about PostScript? Chances are that your main previous exposure to PostScript is through printing errors.
We'll take a fresh look and ask ourselves these three questions:
We'll answer these questions with a triple yes! Let's dig into the details.
PostScript's roots come from Xerox PARC in the late 70s.
Adobe released the first version of PostScript to the market in 1984.
The language fulfilled two needs: prepress software needed to produce a single format for all printers, and printer manufacturers wanted a single document format to interpret. PostScript allowed device-independent, high-quality graphics.
The language developed alongside high-end laser printers and even evolved to screen display in NeXTStep.
When the Internet became widespread, PDF took over as a universal document description and exchange format, inheriting various PostScript concepts.
Here is stairs.ps, along with the image it generates.
As a vector format, PostScript has clear advantages compared to a bitmap format. The file is much smaller, and the contents can be scaled arbitrarily.
We can run a PostScript program with a PostScript interpreter.
The main one is GhostScript.
It will typically turn a PS file into a PDF file with the ps2pdf
command.
Note that the best way to get a complete install on macOS with all options available is not to use brew
but to build it from sources.
GhostScript can also interpret PostScript dynamically.
Just run gs
and start typing code.
The language concepts are very simple and elegant. Indeed, they had to be small enought to be interpreted on printers.
PS works with four stacks:
Here are four valuable pointers that should be sufficient to get started quickly:
So, PostScript is a very simple language with a small yet powerful set of operators.
This talk is not a lecture on PostScript. Instead, let's get a feeling of what it's like by looking at some code.
A drawing then reproduced with my 10-years old kid (Twitter).
Notice the implicit graphic context.
Notice how the setrgbcolor
operator consumes the R B G operands left on the stack.
Notice how the for
operator consumes 4 operands, the 4th being a block of code.
A reproduction of the famous early computer art "Schotter" by Georg Nees. Squares are perfectly aligned on the top row, and then gradually rotate and translate randomly downwards.
Notice that PostScript can use random values with the rand
operator.
Notice the rotate
and translate
geometric transformations.
Notice, again, the conciseness of the code, and the 3 lines minified version.
Another reproduction.
Notice how we define procedures by associating a block of code with a new, in the topmost dictionary.
Notice the transformation matrices, and the clipping
operator to limit drawing to a certain area.
Amstrad CPC color palette: cps.ps.
Notice the geometric transformations.
Notice the gsave
and grestore
operators.
Notice how code blocks - or anonymous functions - can be put on stack and used as operands.
Swissquote logo.
Notice how we can pack operands using a custom encoding sheme, effectively compressing the code to only 4 lines.
Details and write up here.
PostScript can be extremely compact. Consider this infamous PostScript ray tracer by Takashi Hayakawa, winner of the Obfuscated PostScript contest in 1993.
Hayakawa achieved extreme compactness by defining one letters procedures, encoding his program into strings, and parsing them at runtime.
By analyzings his code and using various tricks, I even managed to reduce it further from 760 bytes down to 742 bytes.
So, if PostScript is not only a document description language, if we can write a ray tracer, what is the language actually capable of precisely?
Is it Turing-complete, ie. does it share the same computation model as other advanced languages, such as C++ or Java?
To answer this question, let's take the brainfuck test. Brainfuck is a minimalistic "esoteric" language. Its 8 operators are quite inefficient, but enough to make the language Turing-complete.
So here is bfps, a brainfuck interpreter in PostScript.
Interpreting the PostScript propram will also interpret the brainfuck program.
As PostScript can do everything that brainfuck does, PostScript is a Turing-Complete programming language.
Despite being theoretically as powerful as other languages, PostScript's simplicity comes at the expense of various capabilities. No syscalls, no network access, no threads, no Unicode support, no strong typing.
PostScript can still interact with its environment, mainly with the file system and stdin
, stdout
and stderr
. PostScript can also access various data through the dictionaries made available by the interpreter.
Sometimes, being Turing-complete can be a disadvantage. Indeed, printing requires predictability. It's nice to know the number of pages in advance, and if the program will ever stop.
PDF is not a Turing-complete programming language, but merely a document format that describes document layout. This is much more convenient for printing and sharing documents.
Adobe PDF was heavily influenced by Adobe PostScript.
Here is, for fun and education, a document that is both valid PostScript (stopping after two lines) and valid PDF (parsed starting from the end). It will display different contents when interpreted as PS or PDF.
This explains this fun trick were pdf2pdf
transforms p.pdf into p.pdf.pdf
, both PDF files displaying differents contents.
Ok, so how can we write PostScript?
There's no dedicated IDE and no debugger, except very experimental tools.
That's why I decided to use the macOS SubEthaEdit open-source text editor, and write a PostScript mode for it. I also wrote a custom stylesheet to get this nice syntax higlighting in these slides.
You can run your PostScript code in GhostScript with gs x.ps
. You will also quickly need these basic commands to understand your code by printing the topmost stack item, or the stack as a whole.
Let's wrap up by modifying this little example. We'd like to set the clock handles to the correct position. We can read the current date and time from a dictionary. Complete the code near the question marks.
Also, notice the ifelse
operator, which takes 3 operands: a boolean and two blocks of code. Very elegant.
This example illustrates how we can draw in PostScript by using simple arithmetics and graphics transformations.
Now that you're familiar with the PostScript language, its syntax, its capabilities, let's see if and how we can code games to play against an actual printer.
How can we turn PostScript, a document description language, into an interactive environment?
Let's see how to create a TicTacToe game, and even a complete chess game with its own chess engine.
As a side note, here is the printer used for this research.
Games assume some level of interactivity.
We can communicate with the printer by piping a program directly to the printer on port 9100, and let the communication open for two-ways communication using the dash -
character.
We can also also connect to port 9100 and enter the interactive ("executive") mode by typing:
%PS
executive
In my experience, you have to copy-paste these two lines to reliably get a prompt on the printer.
From the PostScript program's perspective, when can then read the data as in a file.
Demo: tictactoe.m4v
cat ttt.ps - | nc 172.20.10.7 9100
No real difficulty with this program, besides figuring how to establish a two-ways communication with the printer.
Data structure is simply encoded in a string.
The algorithm is simple yet still "fun" to play with.
Notice the numberOfFreePos
procedure, how it leaves and add to its result on the stack, akin the a reduce function.
Let's tackle the next challenge with PSChess, a full chess game with its own "IA".
Demo: pschess.m4v
cat pschess_compact.ps - | nc 172.20.10.2 9100
Let's go through various challenges I needed to solve.
The pieces design is made of moveto
, lineto
and arc
operators plus a few integer operands.
Another challenge is the structure of the code. It turns out that PostScript files can import other PostScript files with (x.ps)
run.
This allows separation of concerns, including unit tests and visual tests.
The data structure is kept simple, the board being encoded as a string.
As the code size grows over a few hundred lines, it gets very hard to remember what operators your procedures consume, and what they leave on the stack.
I chose to use capitalized names for the "main name", and lowercase names surrounded with underscores before and after the "main name".
Now, we can immediately see what a procedure consumes and leaves on the stack. And how it relates with more conventional function definitions.
Basic profiling capabilities can be built with the realtime
operator.
I also had to implement a custom stack trace mechanism, still present in source code.
What about the chess engine? I implemented the simplified Michnievski evaluation function, which basically sums up the cost of each piece plus a score depending on their respective positions on the board.
White will try to play so that the results of the evaluation function increases, while Black play to decrease it.
The "AI" uses the standard minmax algorithm. It basically explores the possible moves chains and plays to minimize the opponent's advantage.
We've seen how to code and play directly with the printer.
Now let's focus on the desktop with Sokoban and Tetris.
Terminal 1:
$ mkfifo /tmp/p; cat sokoban.ps /tmp/p | gs -
Terminal 2:
$ stty raw -echo; cat > /tmp/p
Images can be described with text. Here, each pixel is encoded with its RGB components ranging from 0
to F
(4 BitsPerComponents
is 16 possible values).
(TODO: put the my Python script on Gist)
Notice how ImageMatrix
inverts the y
coordinate, because in PostScript it normally goes upwards.
Levels are described as strings, in a standard format.
Only 7 symbols are needed.
Many more levels can be found online, eg. here or here.
The main challenge was to manage immediate user input.
Chess and TicTacToe require an end of line to send the input.
In Sokoban, we want to react to keystrokes immediately.
This is achieved by concatenating the Sokoban code with a fifo file, and have the interpreter keep on reading stdin
.
In another terminal, we push keystrokes to the fifo file after enabling immediate direct inpout with stty raw -echo
.
From PostScript, input is read as in a file.
A nice feature of Sokoban is the ability to revert your moves.
This is achieved with an undo manager, implemented as a ring buffer.
At each move, we store the state of cells about to change.
My first take was an attempt to replicate some object-programming constructs. It turned out that thinking in PostScript and embracing the language idioms was much more clear and succint.
Terminal 1:
$ gs -sNOSAFER tetris.ps
Terminal 2:
$ stty raw -echo; cat >> t.txt
Tetris has strict implementation guidelines. I chose to base my implementation on Nintendo SNES implementation, dissected here.
Tetriminos are represented as 5x5 boolean arrays.
Their possibles rotations are also stored in arrays, each array indexed on the tetrimino code.
So that, we have very little to keep track of.
Only two numbers, for code and rotation.
Once for the active tetrimino, and another time for the upcoming tetrimino.
The first two rows exist in the program memory, but are not visible. They're just here to allow tetrimino spawning in (5,2).
Here we see the code of each tetrimino.
We use these code to fill the main grid. We add 10 to distinguish the active (droping) tetrimino. We'll substract 10 to each block when it cannot move anymore.
Here again, managing user input is a challenge.
Unlike in Sokoban, we want the code to continue going, even if the user hasn't hit any key. In other words, user input shouldn't be blocking.
We achieve that by appending raw keystrokes to a file.
The PostScript program reads this file continuously, deleting the contents after reading.
A notable feature of Tetris is the usage of a so-called "random bag".
All seven pieces are mixed, and pulled one after the other out of the bag.
This somehow guarantees a certain fairness across games.
Again, we can note how simple and expressive PostScript is.
The previous remark about PostScript expressiveness also stands for levels, speed and scoring management.
We can keep track of high score by saving a high scores file.
I was surprised to notice that we can very easily read and write files on the printer file system, and that the files are persisted across sessions and reboots.
Now we can truly save a game like chess.ps
, connect to the printer in executive mode, and run it with (chess.ps) run
!
This is not all. We can use other tricks to write games.
Here is how to perform animations.
We create a loop in which each frame consists in:
We can even simulate 3D renderings with some sines and cosines.
And add some music, sending MIDI messages to some external MIDI synthetizer (see https://gist.github.com/nst/bccc3dc2d2318cf637e65e1a03c1c9f7).
Which I didn't do because I didn't find a MIDI synthetizer for macOS able to read on stdin
, unlike on Linux apparently.
As a summary:
Thank you for reading.
Don't hesitate to get in touch: nicolas at seriot dot ch.
April 2025: This page supersedes the former one.
EOF