Yes, Doom runs on everything — there's literally a subreddit dedicated to proving it. But one developer asked a better question: what if a single instance of Doom ran in ASCII art, streamed live to thousands of users simultaneously, and let Twitch chat collectively control the character? Sounds simple. It was absolutely not. What followed was a deep dive into custom network compression, ANSI escape code parsing, XOR frame diffing, run length encoding, and Huffman coding — all just to watch Twitch chat fail to walk up a staircase. Repeatedly.

How Does Doom Run as ASCII Art in a Terminal?

The foundation of this whole experiment is a program that converts Doom's video output into ASCII characters rendered with ANSI color codes. ANSI codes follow a very specific format: an escape character, followed by an open bracket, followed by a command. For example, 1;1H moves the cursor to the top-left of the screen, 2J clears it, and 38:2:116:001:001m sets the text color to dark red. Every color on screen gets its own escape sequence, and the character chosen to represent each pixel is determined by the brightness of that pixel — darker areas use smaller, sparser characters, while brighter areas use dense ones like W or $.

The raw ANSI escape codes required to display a single frame of Doom in color 01:45 The raw ANSI escape codes required to display a single frame of Doom in color Watch at 01:45 →

To parse a frame out of this stream, the developer looked for specific control sequences — carriage returns, newlines, and the ;H cursor-home command — that signal the end of one frame and the beginning of the next. Once a frame is cleanly parsed, it can theoretically be sent up to a relay server and distributed to every connected client. That's the concept. The execution? That's where things got expensive fast.

How Do You Cut Streaming Bandwidth from 1 GB/s to Almost Nothing?

Before any compression, the raw math was brutal. Each on-screen cell requires 3 bytes for RGB color and 1 byte for the ASCII character. At a resolution of 212 columns by 66 rows, that's roughly 56,000 bytes per frame. At 30 frames per second with 1,000 simultaneous viewers, you're looking at about 1 gigabyte per second of outbound data — or somewhere between $5 and $150 per hour depending on your cloud provider. That's a hard no.

Compression benchmark results after each optimization stage 04:10 Compression benchmark results after each optimization stage Watch at 04:10 →

So the developer ran 10,000 frames of Doom as a benchmark and measured compression at every stage. The baseline, sending everything raw, came in at 545 megabytes for 10,000 frames. Then he started cutting.

  • Duplicate character removal: ASCII Doom renders each character twice side by side. Drop the duplicate and regenerate it client-side. Down to 272 MB.
  • Drop the character entirely: Since the character is determined by color brightness, just send the color and recalculate the character on the client. Down to 204 MB.
  • Color quantization: Instead of 3 bytes per color (one each for R, G, B), pack all three into a single byte — 3 bits for red, 2 for green, 3 for blue. It looks slightly uglier, but down to 68 MB.
  • Deduplicate identical frames: Doom's loading screens barely change. Why send the same frame twice? Skip duplicates entirely. Down to 35 MB.

That's a dramatic improvement before even touching real compression algorithms. But the developer wasn't done.

What Is Run Length Encoding and How Does It Work?

Run length encoding (RLE) is one of the oldest tricks in data compression, and it's beautifully simple. If you have the string WWWWWDDMEGADOOR, instead of storing every character individually, you scan for runs of the same character and store the count followed by the character: 5W 2D 1M.... For strings with long repeated sequences, this is a massive win. For strings that don't repeat, it can actually make things slightly worse — encoding a single character now takes two bytes instead of one.

Huffman tree construction visualized step by step 07:30 Huffman tree construction visualized step by step Watch at 07:30 →

In ASCII Doom frames, there are long horizontal runs of the same character — stretches of the same wall color, the same sky, the same floor. Applying RLE brought the total down from 35 MB to 28.9 MB for 10,000 frames. A solid gain, but there was a smarter move waiting.

How Does XOR Make Video Frame Compression Faster?

XOR (exclusive or) is a bitwise operation with a fascinating property: it has memory. If you XOR value A with value B, you get a result C. XOR that result C with either A or B and you get the other one back. This is the principle behind things like swapping two variables without a temporary, and it's also how forward error correction works in WebRTC — where one extra XOR'd packet at the end of a group can reconstruct any single dropped packet.

Applied to video frames, XOR becomes a powerful delta encoding tool. Take frame zero and XOR it with frame one. In a game like Doom, where consecutive frames are often nearly identical, most of the resulting bits will be zero — because XORing two identical bits always produces zero. Only the parts that actually changed between frames produce non-zero values.

Twitch chat getting repeatedly stuck on the same staircase 11:20 Twitch chat getting repeatedly stuck on the same staircase Watch at 11:20 →

Now combine XOR delta encoding with run length encoding: you end up with long runs of zeros, which RLE compresses extremely well. The result dropped the total from 28.9 MB to 21.8 MB for 10,000 frames. That's real, meaningful compression — not just trimming redundancy, but exploiting the temporal coherence of video.

How Does Huffman Encoding Shrink Data So Efficiently?

Huffman encoding is a variable-length entropy coding algorithm. The core idea: characters that appear frequently get shorter binary codes; characters that appear rarely get longer ones. To build a Huffman tree, you start by counting the frequency of every symbol in your data, then use a min-heap to repeatedly combine the two lowest-frequency nodes into a parent node until you have a single tree.

Reading the tree from root to leaf — assigning 0 to every left branch and 1 to every right — gives each character its unique binary code. The critical property: because of how the tree is structured, no code is a prefix of another. That means decoding is unambiguous — you just walk the tree bit by bit until you land on a leaf, and that's your character.

In the original example used to explain this, the string abcabdaa goes from 7 bytes down to 2 bytes after Huffman encoding. Applied to the Doom compression pipeline, Huffman coding brought the total down to 15.99 MB for 10,000 frames. Stack every technique together — quantization, deduplication, XOR diffing, RLE, and Huffman — and the final number lands at a remarkably lean 13 MB for 10,000 frames.

That's roughly 1.3–1.5 GB per 5.5 minutes of gameplay. Suddenly, running this for real users became financially sane.

How Do You Parse ANSI Escape Codes From a Running Game?

The developer wrote a program that spawns the ASCII Doom process, reads its raw output stream, and scans for specific ANSI control sequences to detect frame boundaries. The key markers are carriage returns, newlines, and the cursor-home command ;H. When the parser sees these, it knows a full frame has been assembled and can package it for transmission.

The parsed frame data — after all the compression stages — gets sent up to a relay server. The relay holds connections open for every client and fans the compressed frame data out to all of them simultaneously. Each client decodes the compression stack in reverse: Huffman decode, RLE expand, XOR reconstruct, regenerate characters from brightness, and double every cell. The result is a smooth ASCII Doom experience rendered entirely in a browser or terminal.

How Did Twitch Chat Collectively Control Doom?

The input system is elegantly simple. Twitch chat floods in with commands — w, a, s, d, shoot, whatever players type. Every 250 milliseconds, the server samples that flood, finds the most frequently typed command in that window, and treats it as the chosen action. That winning input gets fed into the game at a rate of once every 16 milliseconds — simulating a held key — until the next 250ms voting window closes and a new winner is picked.

It's a clean, democratic, and gloriously chaotic system. And the results were exactly what you'd expect from collective human intelligence applied to a first-person shooter: Twitch chat got stuck on stairs. Multiple times. Then got stuck again. But — and this is the important part — they did eventually beat the level. On the lowest difficulty setting, yes, but they beat it. A thousand people, controlling one guy, walking through one door. Together.

The whole experiment required a generous friend (shoutout to Stuart at stuartdev) providing a high-bandwidth server for free, because even at 13 MB per 10,000 frames, running this at scale for real users adds up. But it worked. ASCII Doom, streamed live, controlled by Twitch chat, compressed with a custom-built pipeline of XOR, RLE, and Huffman encoding. Engineering at its most gloriously unnecessary — which is to say, at its absolute best.