I built Hacking Slash in 24 hours at HackMerced — a dungeon crawler in Pygame where you pick a hero, fight through waves of enemies, and try not to die. It worked great on my laptop. The problem was that my laptop was the only place anyone would ever play it.
"Hey, download this ZIP, install Python 3.11 — no, not 3.10, it has to be 3.11 — then run this script in your terminal" is not a pitch that makes people drop what they're doing. You know what does? A link. That realization — that the browser is a distribution channel, not just a platform — sent me on a journey through three very different approaches to getting a Pygame game into the browser. This post is the full story, including the parts where I stared at black screens and questioned my life choices.
Why Put Your Pygame Game in a Browser?
Let's start with the obvious: nobody wants to install your game. I don't mean that cruelly — I'm sure your game is lovely. I mean that every step between "I heard about your game" and "I'm playing your game" loses people. App stores lose 90-97% of viewers before they ever hit the install button. Even after install, only about 30% of players finish a tutorial. Your game could cure loneliness and most people would bounce at "Requires 200MB download."
Browser games skip all of that. There's no download, no install, no account creation, no app store approval. Someone clicks a link and they're playing. It's the impulse purchase of game distribution. The numbers back this up pretty dramatically.
And it's not just indie platforms. The broader trend is moving this direction fast. GDC's 2025 survey found that 16% of developers now target web browsers — the highest in a decade, up from 10% in 2024. Poki hit a billion plays per month in mid-2025. The HTML5 games market is projected to nearly double over the next decade.
So the case is clear. The question is how. If you've got a Pygame game, there are three realistic options — and I've tried two of them personally.
Option 1: Pygbag (Keep Your Python)
Pygbag is the "have your cake and eat it too" option. It compiles your Pygame code to WebAssembly via Emscripten, then runs it in the browser. You keep writing Python. You keep using Pygame. You just sprinkle in some async and await pixie dust, and theoretically your game works on the web.
Theoretically.
Here's the core change Pygbag requires. Your main loop goes from this:
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((0, 0, 0))
pygame.display.flip()
To this:
async def main():
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((0, 0, 0))
pygame.display.flip()
await asyncio.sleep(0)
asyncio.run(main())
That await asyncio.sleep(0) is the whole trick. Without it, you get a black screen — the browser never gets a chance to render. And "black screen with no error message" is Pygbag's signature move. There are at least seven open GitHub issues about it, each with a completely different root cause. It's like a mystery novel where every chapter reveals a new suspect.
I tried Pygbag with Hacking Slash. I pushed five commits in a single day trying to make it work — adding __main__.py, converting to async, adding Emscripten detection, restructuring imports. Five commits. Same day. Each one a new theory about what was wrong. Each one greeted by the same black screen, silently judging me. None of it got me to a working build.
The Gotcha List
After my own experience and a deep dive into community issues, here's what you're signing up for with Pygbag. Grab a beverage — this list is long:
- Audio must be OGG only. No WAV, no MP3, no M4A. And even then, audio on iPhone essentially doesn't work — iOS doesn't natively play OGG in this context. Hope your players enjoy the sound of silence.
- No threading, no multiprocessing, no networking. The browser sandbox means any standard library modules that touch the OS are gone.
- No nested game loops. If you have a separate loop for a menu or cutscene, you need to flatten everything into a single async loop with state management.
- No fullscreen mode. Use a fixed resolution (1024x600 is the recommendation).
- No code after
asyncio.run(). Nosys.exit(), nopygame.quit()after the loop. - Silent import failures. If you import a module Pygbag doesn't support, you don't get an error — you get a black screen. Our old friend, the black screen. It doesn't even have the courtesy to tell you why.
- CDN runtime dependency. Pygbag's WASM interpreter isn't bundled with your game — it's downloaded from GitHub at runtime. When Pygbag updates and drops support for older versions, shipped games break without you touching them. Imagine waking up to "your game is broken" messages when you haven't pushed code in months. This hit multiple developers on itch.io in August 2025.
- Single maintainer. Pygbag has about 1,700 weekly downloads on PyPI and one primary developer. That's a bus factor concern for anything you plan to maintain long-term.
When Pygbag Does Work
Okay, I realize I just wrote eight bullet points of doom. Pygbag isn't evil — it's genuinely useful for a specific niche: small 2D games with simple asset footprints. Game jam entries, puzzle games, simple arcade games — things where you can live within the constraints and where the speed of "just add async" outweighs the limitations.
If your game is small, already structured with a clean single loop, uses OGG audio or no audio, and you don't need mobile support — Pygbag is probably fine. But if you've hit any of the gotchas above and you're five commits deep with no progress, that's not a skill issue. That's a signal. Ask me how I know.
Option 2: PyScript + Pygame-CE
PyScript (built by Anaconda) runs Python in the browser via Pyodide — essentially CPython compiled to WebAssembly. You can write a <script type="py-game"> tag and have Pygame-CE running in a browser with zero install for developers. On paper, this is the dream. Python in a script tag. Someone call the Nobel committee.
It's also explicitly labeled "experimental" in the official docs, which in software is code for "this might eat your homework."
The big one for games: PyScript uses setTimeout for its game loop, not requestAnimationFrame. That might sound like a minor API difference, but it means the browser isn't syncing your frame updates with the display refresh. For a puzzle game, you won't notice. For anything with smooth animation or real-time action, the difference is visible.
There are other constraints too. Audio requires OGG format (same as Pygbag). Assets need to be explicitly declared in PyScript's configuration. The pygame-web wiki is direct about it: "not suitable for heavy games, music or 3D." And Pygame-CE itself has had buildability issues in the Pyodide environment — builds were temporarily removed and then restored after fixing Emscripten support.
That said, the PyScript team is actively working on this. There was a PyCon US 2025 talk specifically about building browser-native games with WebAssembly. The Pygame-CE community has an open proposal (Issue #3187) for a native main loop API that would work on both desktop and browser without async hacks. If that lands, it could change the equation.
My take: PyScript is worth watching, not worth betting on today. If you're reading this in 2027 and PyScript has stabilized its game loop and Pygame-CE has landed that native browser API, come back and laugh at how wrong I was. For right now, it's an experiment — a very cool experiment, but not one I'd ship a game on.
Option 3: Port to a Browser-Native Framework
This is the option that sounds like the most work. "Just rewrite your entire game in a different language" is advice that would get you laughed out of most conversations. But hear me out — because if your Pygame game is small enough to be portable (and most hackathon games are), rewriting it in a browser-native framework like Phaser gets you native performance, native input handling, native audio, and the entire web ecosystem for free.
This is what I did with Hacking Slash. And despite what the previous two sections might suggest about my decision-making, this one actually worked out.
The Two-Day Rewrite
After five failed Pygbag commits on October 7th, I did what any rational developer would do: I rage-quit and started rewriting the entire game in JavaScript. (I'm being slightly dramatic. It was more of a "calm, measured pivot" that happened to occur right after I closed my terminal in frustration.)
By the end of that same day, I had a working Phaser game with combat, enemies, and the core gameplay loop. By October 8th, I had full parity with the Pygame version. Then I spent the rest of that day — twenty-plus commits — fixing edge cases: movement speed, attack timing, projectile physics, boss behavior. The kind of stuff that makes you say "close enough" at 2am.
Total elapsed time from "Pygbag isn't working" to "the Phaser version is live and playable": about two days. That's for a hackathon-sized game with a clear, well-defined game loop. A larger game would take proportionally longer, but the point stands — if the scope is right, this is faster than you'd think. I spent more time fighting Pygbag than I did rewriting the game.
For the full story of the port itself, I wrote a separate Hacking Slash devlog that goes deeper into the technical details.
What Translated Cleanly
A surprising amount of Pygame translates conceptually to Phaser, even though the language and API are completely different:
- Sprite groups became Phaser groups (
this.physics.add.group()) — nearly 1:1 - Collision detection moved from manual rect checks to Phaser's arcade physics, which was actually simpler
- The game loop (update/draw) maps directly to Phaser's scene lifecycle:
update(time, delta) - Tile-based dungeon layouts carried over directly — Phaser has excellent Tilemap support
What Didn't
Input handling was the biggest surprise. Pygame gives you raw keyboard events and you do what you want with them. Phaser has its own input system that works differently on desktop vs mobile — and mobile is where it gets interesting, because touch controls don't exist in a desktop Pygame game at all. I had to build a virtual joystick and action buttons from scratch.
Asset loading was the other big shift. Pygame loads images synchronously with pygame.image.load(). Phaser uses an async preloader — you load everything in a preload() function, and the scene doesn't start until assets are ready. It's actually a better pattern (no more random hitches when an asset loads mid-gameplay), but it means restructuring your initialization flow.
And then there's the language shift itself. Python's class system and JavaScript's prototype/ES6 class system work differently enough that you'll hit scoping bugs with this. If you've never debugged a JavaScript this issue, congratulations on your blessed life. The rest of us know the pain.
Performance Tips for Phaser
A few things I learned that apply to any Pygame-to-Phaser port:
- Use texture atlases. With 210 individual sprite textures, Phaser makes 212 draw calls. Pack those same sprites into an atlas and it drops to one draw call. The difference is massive on mobile.
- Pool your objects. Creating and destroying game objects every frame is expensive. Object pooling — reusing "dead" objects instead of creating new ones — can improve fps by 15% or more.
- Compress your build. Brotli compression is 15-20% smaller than Gzip and can cut initial download times significantly. If your hosting supports it, use it.
Which Approach Should You Pick?
Here's my honest take, after living through two of these options and researching the third so you don't have to:
If your game is small, simple, and you just want it online fast: try Pygbag first. Give it an afternoon. If it works, great — ship it and celebrate. If you're three hours in and staring at a black screen, close your laptop, go outside, and consider Option 3. The sunk cost isn't worth it. Trust me on this one.
If you're starting a brand new project and you want it in the browser: skip Pygame entirely. Start with Phaser (or another browser-native framework). You'll save yourself the translation step and get native performance from day one.
If you have an existing Pygame game and the browser matters to you: port it. Especially if it's hackathon-sized or game-jam-sized. The rewrite is less work than you think, and the result is better than what Pygbag or PyScript can give you today.
If you're interested in the Python-in-browser future: keep an eye on PyScript and Pygame-CE's browser API proposal. It's not ready yet, but the trajectory is promising.
Where to Host Your Browser Game
Once your game runs in a browser, you need somewhere to put it. The good news: you have options. The bad news: you have options.
itch.io is the default choice for indie games. Free hosting, unlimited bandwidth, a built-in audience of 118 million monthly visits, and support for donations or paid downloads. The downside is you don't get a custom domain on the free tier, and your game lives inside an iframe.
GitHub Pages is free, fast, and dead simple if your game is already in a repo. You get 100 GB/month of bandwidth (soft limit) and it's backed by a global CDN. The constraint is a 1 GB repo size limit and no server-side logic.
Your own site (which is what I did, because apparently I enjoy making things harder for myself) gives you full control. Hacking Slash lives at penguinboisoftware.com/hackingslash as part of my main site, deployed via AWS Amplify. More work to set up, but you own the URL, the analytics, and the presentation.
Poki and CrazyGames are curated platforms — you submit your game, and if accepted, it goes in front of their massive audiences (100M and 35M monthly players respectively). These are revenue-share models, and some studios are making serious money through them. Worth exploring if your game has broad appeal.
The key insight across all of these: browser games have URLs. They're indexable by search engines, shareable in a chat message, embeddable in a tweet. Native games locked behind app stores don't have that. Over 60% of players discover games through Google searches — having a URL is a genuine distribution advantage.
The Browser Is a Distribution Advantage
I started this journey because I built a game at a hackathon and the only person who played it was me. I ended it with a version of that game that loads in seconds, runs on any device, and deploys when I push to git. That's a pretty good trade for two days of work and one mild existential crisis.
The numbers say the same thing at scale. Browser games are growing faster than any other platform category. The install barrier is real and measurable, and removing it changes how many people experience your work.
If you're sitting on a Pygame game that only runs on your laptop, the browser is the single biggest thing you can do for its reach. The path might be Pygbag (if you're lucky), it might be a Phaser rewrite (if you're impatient like me), or it might be waiting for PyScript to mature (if you're patient, which — have we met?) — but the destination is the same.
Ship to the browser. It's where the players are. And unlike your pip install instructions, they'll actually click the link.
For more on why browser deployment matters and other lessons from building games solo, check out 5 Things I Learned Making Games Solo.