A playable copy of Minecraft Classic, hidden inside a single X thread.
I found this rather cool trick from @rebane2001 on twitter yesterday that reminded me that I was working on this "hard problem" I left 3 months ago of running Minectaft from a tweet.
If you do x.com/compose followed by the URL string on your post itself, it'll open up the compose box with those words clearly printed out! Using this I was able to truncade the instructions and my custom decoder (because it's ~20 tweets that need to be consolidated) base64 encoded
Is it useful? No, is it a really fun sunday hack that made me smile? sure
Play it
Three ways, easiest first:
- Just play → kuber.studio/minicraft
- From the thread: open tweet 2's link, copy the
data:text/html;base64,…line it shows, and paste it into your browser's address bar → that's the decoder. Hit auto-fill from github, then Play. - By hand: in the decoder, paste tweets 3–20 (the raw chunks) yourself, then Play.
How it works
The game is ~9 MB of HTML/JS. A thread is ~20 posts. The squeeze:
| stage | size |
|---|---|
original monolithic game (index.html) |
9.2 MB |
re-minified, JS-aware (minified-index.html) |
4.47 MB |
Babylon.js tree-shaken + dead crypto stripped (game-shaken.html) |
2.35 MB |
| LZMA compressed | 320 KB |
| base64url text | ~427 K chars |
| → 18 game posts + 1 decoder + 1 intro | 20 tweets |
The layout of the thread:
- Tweet 1 — the intro (a compose link that opens the write-up).
- Tweet 2 — the decoder: a
data:text/htmlpage, deflate-compressed and wrapped in a self-inflating bootstrap so the whole thing (a UI plus an embedded LZMA engine) fits in one post. Works fully offline. - Tweets 3–20 — the game itself, as raw
base64url(LZMA)text. X Premium's 25 K-char limit lets each post carry a big chunk directly — no links needed.
The decoder gathers the parts (paste them, or one-click auto-fill from github),
base64url-decodes, LZMA-decompresses, and document.writes the game. A localStorage
shim is injected first, because the game needs storage and data: URLs disable it.
What's where
game-shaken.html the shipped game — Babylon tree-shaken, crypto-stripped, Elon-grass
index.html original monolithic Minecraft Classic (unminified, 9 MB)
minified-index.html the full game, re-minified with a JS-aware minifier
tweet.txt the v1 approach — bytes hidden as invisible Unicode variation selectors
font/ bitmap font the game uses
TWEETS/ the postable thread — tweet-001..020.txt + manifest.txt
unlinked-tweets/ the decoded text= of each tweet (for local testing without X)
FINDINGS.md empirical notes (WAF behaviour, URL limits, compression)
scripts/
build_thread.py game.lzma -> TWEETS/ + unlinked-tweets/ (builds the thread)
build_decoder.mjs decoder_page.html -> self-inflating decoder_bootstrap.html
decoder_page.html decoder source (UI + embedded LZMA engine)
decoder_bootstrap.html the built decoder, embedded in tweet 2
lzma-d-min.js LZMA decode engine (shipped inside the decoder)
intro.txt tweet 1 text (edit this — not the generated TWEETS/)
compose_link.py any text/file -> an x.com/compose/post link
encode.py / decode.py v1: hide bytes as Unicode variation selectors
thread_pack.py / _unpack generic file <-> deflate-thread helpers
Editable sources: scripts/intro.txt (tweet 1), scripts/decoder_page.html (the
decoder), and game-shaken.html (the game — e.g. textures). TWEETS/ and
unlinked-tweets/ are generated — regenerate them with build_thread.py.
The journey (a.k.a. everything that went wrong)
It started as "can we make the broken minified build run" and turned into a compression / X-limits rabbit hole. Highlights:
-
The minifier that ate a regex.
minified-index.htmlthrewUncaught SyntaxError: Invalid regular expression flags. A naïve minifier had seen the//inside a/\//gregex, mistaken it for a line comment, and deleted the rest of the line. Re-minified with a JS-aware tool. -
The compose-link rabbit hole. A link to
x.com/compose/post?text=…opens a pre-filled composer. But X's WAF 403s any URL containing<script(so payloads are base64), and the URL 431s past ~14 K chars — dropping to ~10 K once a logged-in browser's cookies are added. All measured against x.com directly. -
base64url. Standard base64's
+ / =get %-expanded 3× inside a URL. base64url (A–Z a–z 0–9 - _) is URL-safe with zero expansion — 6 bits/char, the density ceiling. -
The white screen. The decoder booted from
file://in tests but white-screened as a realdata:URL — the game readslocalStorage, which is disabled in opaquedata:origins. Fixed by injecting an in-memory storage shim before the game runs. -
Babylon.js is 69% of the game. The bundle's biggest module is Babylon.js 4.0.3 (2.37 MB), but the game only touches 26 of its APIs. Tree-shaking it to that surface (→ 806 KB), plus stripping a dead crypto polyfill (elliptic / bn.js, dragged in by a single
randomBytescall), roughly halved the game — verified still booting, rendering, and opening menus in headless Chrome. -
Beating deflate. Browsers only inflate gzip/deflate natively — but the decoder ships its own engine, so LZMA (−25% vs deflate) does the compression, wrapped so the decoder still fits in one tweet.
-
The Premium plot twist. The compose-link trick beats the 280-char free limit — but X Premium already grants 25 K chars per post. So for the game data, posting raw text directly beats links (no ~10 K URL cap): 45 → 20 tweets.
-
GitHub Pages hated us. Legacy Jekyll choked on the huge inlined-HTML files (stuck builds, failed deploys). A one-line
.nojekyll→ serves static, builds in ~15 s. -
Elon grass. Because why not. Swapped the
grassentry (16×16 green) in the game's__TEX__table for a 64×64 render and re-ran the pipeline. Cost: +1 tweet.
The full measured notes live in FINDINGS.md.
Credits & legal
- Minecraft is © Mojang / Microsoft. This is an unaffiliated technical-art demo, not endorsed by them.
- The compose-link trick: @rebane2001.
- Minecraft Classic Forever - Goats for helping me find the actual source to play with
- Made with funmaxxing by Kuber Mehta.
