We turned web pages into Snake mazes. The hard part was finding the empty space.
Prajjwal Pathak20 min
- date
- words
- 3 923
- read
- 20 min
- sources
- 11 sources
- games
- 8 games
- unit tests
- 393 unit tests
- idea → code
- 49 days
- code → store
- 19 days
- package
- 144 KB
- revenue
- ₹0 revenue
- views
- 1 views
On 28 July we wrote down an idea: click a button and the page you’re reading becomes a Snake level. The snake moves through the empty space between paragraphs. The words are walls.
It sat in a notes file for 49 days. On 15 September we built it, and on 4 October Page Arcade went live on the Chrome Web Store. It now has eight games, and version 1.1.0 shipped on 22 October.
The games turned out to be the easy part. The hard part was one question the notes file answered wrongly: where on a web page is the empty space? A person can tell at a glance. Code has to decide 12 pixels at a time, on a page it has never seen, and it has to be right. If it marks a word as empty, the snake slides through text. If it marks empty space as a wall, the snake dies on nothing, and the player blames the game.
This post covers how we answered that question: the screenshot-first freeze, the grid, the rule that lets a snake cross a 1-pixel line but not a word, and the bug report from the owner dying on ChatGPT next to an icon they never touched. It also covers the part that doesn’t flatter us: this product broke our own rule, and it still earns nothing.
TL;DR
- The page is never edited. It’s photographed, and the game is played on the photograph. The extension takes a PNG screenshot before it injects anything, then reads the page’s layout, then lays one overlay on top. Press Esc and the page is exactly as it was.
- “Empty” is decided by two tests that cover each other’s blind spots. A 12-pixel cell is a wall if its pixels vary by more than 24 in any colour channel, or if the browser says text or media sits there. The pixel test sees what’s drawn. The layout test catches very faint text the pixel test would pass.
- Thin lines had to become passable, and that took a classifier. Before it, a 1-pixel
<hr>walled off two rows of cells and split the board in half. After it, there were 0 deaths in 1,456 scripted crossings of lines and borders at three zoom levels. A run of underscores still blocks the snake, because underscores are content. - The best bug report was a death on ChatGPT. Invisible button boxes, hidden screen-reader labels and text boxes grazing a cell by one pixel were all being treated as walls. On a ChatGPT-like test page, walls with nothing drawn in them went from 124 of 472 to 0 of 345.
- A random empty cell is not reachable. The original notes said it was. The free space on a page is a set of islands, so the game keeps the largest island, joins the second one with a portal when it is big enough, and refuses to start on pages with fewer than 80 free cells.
- It asks for three permissions and no site access.
activeTab, granted by your click, is enough to take the screenshot and inject the game. The extension does nothing on any page until you ask it to.
Page Arcade, in one paragraph
Page Arcade is a Chrome extension. Click its icon, or press Alt+Shift+P, and the page you’re on freezes and becomes a game level. There are eight games.
- Snake lives in the page’s empty space; the edges wrap, and thin lines can be crossed.
- Word Blaster: the page’s own words fall in waves and you shoot them from a rocket.
- Type Storm: you type the falling words to destroy them.
- Brickout: every word on the screen is a brick.
- Whack-a-Word: click the words that pop out of the page.
- Page Roller: roll the page up, smallest words first.
- Gravity Crumble: knock words loose and let the page collapse.
- Page Chomp: eat the dots in the empty space while four ghosts chase you.
There is no account. Scores, XP, 23 achievements and 18 skins live in your browser. Per-site leaderboards and share links are opt-in.
The idea is old. That’s the point.
We weren’t the first to play on web pages, and the research file we wrote on day one says so.
- Kick Ass (2010) put an Asteroids ship on any page and let you shoot the elements.
- Katamari Hack (2011) rolled a page’s words up into a growing ball.
- Google Gravity (2009) dropped the Google homepage into a physics engine.
- PlayAnyPage, a bookmarklet from April 2026, has Brickout and a Snake that lives in text. It’s the closest thing to what we built.
What the hits had in common was instant play on the page you were already on. That set the bar. There’s no level select and no loading screen: the game starts on whatever page you’re reading. It also set the hard constraint. If the level is whatever page you happen to be on, the code has to understand pages it has never seen, and it gets one try.
Step one: freeze the page without touching it
The obvious approach is to inject a canvas over the live page and run the game on top of it. That fails in small ways. Carousels keep rotating. Videos keep playing. The page’s own keyboard shortcuts fire when you press an arrow key. And if the game edits the page, removing words as they’re shot for example, there’s no clean way back.
So the page is never edited. It’s photographed, and the photograph is played on.
- Capture first. When you click, the background worker calls
chrome.tabs.captureVisibleTabas a PNG, before anything is injected, so the image is the page and not our overlay. It has to be PNG: the free-space test compares exact pixel values, and JPEG noise looks like content. - Read the layout. A content script asks the browser where every line of text and every image, video, canvas, SVG and form field is drawn. This happens before the overlay exists, otherwise every lookup would hit the overlay itself.
- Mount one overlay. It’s a single custom element on
<html>, not<body>, because a transform on<body>would breakposition: fixed. It uses a closed shadow root and the maximum z-index, and holds two canvases: the backdrop (the screenshot, redrawn only when a word is destroyed) and the game layer, redrawn every frame. - Freeze, don’t disable. Keys are swallowed at the capture phase, but Ctrl, Alt and Meta shortcuts and the F-keys still pass through. Playing videos and audio are paused. Scrolling is pinned to the current position rather than switched off:
overflow: hiddenwould remove the scrollbar, reflow the page and make every measurement we had just taken wrong.
Press Esc and the overlay goes, the listeners go, the scroll position comes back and paused media resumes. The page never knew.
Destroyed words are painted into a copy of the screenshot. The original bitmap is never drawn on, so Retry can restore the page exactly.
One measurement trap: the screenshot’s scale comes from screenshot.width / innerWidth, not from devicePixelRatio. Under browser zoom and Windows display scaling, the two disagree, and the size of the captured image is the only number that is always right.
Step two: what counts as empty
The grid cuts the overlay into 12 CSS-pixel cells, centred so the leftover strip is split between both edges. Each cell has to be classified as free or wall, and there are two ways to be wrong.
The pixel test. For every cell, take its screenshot pixels and measure the spread (max − min) in each colour channel. If any channel varies by more than 24, something is drawn there, and the cell is a wall. Uniform means empty, whatever the colour. Dark themes, tinted panels and gentle gradients all count as space. Text, icons, borders, images and drawings don’t. Cell boundaries round outwards, so a line sitting exactly on a cell edge blocks both neighbours. A free cell that is really a border is the worse of the two mistakes.
The layout test. The browser knows exactly where every line of text is drawn: Range.getClientRects() returns a rectangle per line. It also knows the box of every image, video, canvas, SVG, iframe and form field. A cell those rectangles overlap is a wall. This catches what the pixel test misses: very low-contrast text that varies by less than 24 against its background.
Neither test is enough alone. The pixel test can’t see grey-on-grey text. The layout test can’t see a background image with text baked into it. Together they mostly agree with what a person would call empty.
On a Wikipedia article at 1,366 pixels wide, this produces something you might not expect: the article itself is solid wall. 12-pixel cells don’t fit between lines of body text, so the snake lives in the gutters, the margins and the gaps around headings. That turned out to be the right game. The page’s layout becomes a maze, and every site’s maze is different.
Step three: the snake must not die on a line
The first version had a bug the owner found in minutes. The snake died on <hr> dividers and thin borders.
Under the rules above that was correct: a 1-pixel line makes a cell non-uniform, so it’s a wall. Because boundaries round outwards, a 1-pixel rule actually walled off two rows of cells. On a page with a divider across the middle, the board was cut in half.
A person sees a thin line as decoration, not as a wall. Teaching the grid the same thing took a classifier, and most of the work was in what it must not free.
A wall cell becomes a passable line cell only if all of these hold:
- one background colour covers at least 60% of the cell;
- every “ink” pixel lies in at most one horizontal and one vertical band, each at most 3 CSS pixels thick (plus anti-aliasing), so dashed and dotted lines still count;
- the band touches a cell edge, because a hyphen in the middle of a cell isn’t a border;
- the band is a single colour.
The layout data adds hints. <hr> elements and element borders of 3 pixels or less loosen the rules where they overlap, so dashed borders whose dashes stop short of the cell edge still pass. Hints only loosen the rules; they never free a cell on their own. A final pass frees rounded corners whose neighbours on both sides are lines.
Text is never a line. An underscore, a row of dashes or an icon’s straight stroke looks exactly like a thin border to a pixel classifier. So any cell where the browser says text, an icon or a button is drawn is ruled out before the classifier runs. That single rule removed the worst class of bug: snakes sliding through ________ in a form.
The evidence is in the tests: 16+ classifier cases, including 1-pixel and 3-pixel lines (free), 4-pixel lines (wall), 2× scale, dashed, dotted, a hyphen, a stray pixel, two colours, anti-aliasing, a shadow, text over a line and a dark theme. A headless test page full of <hr>s, bordered divs and a table was crossed 1,456 times by a scripted snake at 1×, 1.25× and 2× zoom, with 0 deaths. On one Wikipedia article, 536 cells that used to be walls became crossable lines.
Press G during Snake and the game paints its own reading of the page: walls in red, line cells in cyan. It’s a debug view we left in, because it’s also the fastest way to tell whether a death was fair.
The death on ChatGPT
On 17 September the owner died playing Snake on ChatGPT, next to a copy icon they hadn’t touched. They sent a screenshot of the G view. Red cells covered empty space.
There were four causes. All of them were walls made of nothing:
- A button’s box is bigger than its icon. A copy button is a 32-pixel box around a 20-pixel icon. Any cell the box touched by half a pixel became a wall, which put a band of invisible wall around every row of icons.
- Text boxes grazing a cell. A text line’s rectangle that clipped a 12-pixel cell by one or two pixels walled the whole cell.
- Visually hidden text. Screen-reader labels and transparent tooltips are invisible, but they still have layout boxes. Three of them sat in ChatGPT’s empty sidebar header, and each one was a wall.
- Icons read as lines. Once icons stopped being box walls, their straight strokes started passing the line classifier. The share and regenerate icons showed up cyan.
The fixes:
- A layout rectangle now counts only if it reaches at least 3 pixels into a cell on both axes.
- Text rectangles lose their empty top 15% and bottom 10% before they’re tested.
- Icons, buttons and text are walls only if something is actually drawn in the cell, and they’re never eligible to be lines.
We measured the result as the number of wall cells whose pixels are blank, on a local test page built to look like ChatGPT: a reply, a row of 32-pixel icon buttons, a composer, a hidden label and a transparent tooltip. Before: 124 of 472 walls had nothing drawn in them. After: 0 of 345, with 0 line cells inside any icon or button. On Wikipedia’s Snake article the count went from 210 to 96. The 96 left are flat dark areas inside a photo, correctly walled by the image’s box.
A game is fair when the player can see why they died. Every one of these bugs was a death the player couldn’t see a reason for.
A random empty cell is not reachable
The original notes from July had one confident line in them: put food on any random free cell, and it’s “guaranteed reachable.”
It isn’t. The free space on a web page isn’t one space. Margins, gutters and paragraph gaps are islands, walled off from each other by text. Food in the left margin is unreachable from a snake in the right margin.
So the grid is flood-filled into labelled islands, 4-connected and never diagonally. Since Snake’s edges wrap, the fill wraps too: a left margin and a right margin that meet at the screen edge become one island. Then:
- The largest island is the arena. Food and power-ups only ever land on it, so everything on the board is reachable.
- If the second-largest island has at least 30 cells, a pair of portals joins it to the arena.
- With fewer than 80 free cells, or no straight run of 5, the game doesn’t start. It tells you there isn’t room and suggests scrolling to another part of the page.
We kept the wrong line in the notes file, with a pointer to the correction. The mistake is worth remembering: it sounded obvious, and it would have produced a game where food sometimes appears where you can’t get to it.
Eight games, one grid
Once the grid existed, the other games came quickly, because each one is a different question asked of the same page:
- Snake and Page Chomp use the free cells. Page Chomp’s dots and ghosts live in the same islands Snake does, and the four ghosts chase with the classic arcade personalities: one targets you, one aims four cells ahead of you.
- Word Blaster, Type Storm and Brickout use the text rectangles. Each word on the screen is a target or a brick at the exact position it was drawn. Word Blaster names its waves after the page’s own section headings.
- Page Roller and Gravity Crumble use the words as physical objects. Gravity Crumble is the only game that uses a physics library (Planck.js, a JavaScript port of Box2D). Everything else runs on our own small kit: a spatial hash, swept circle-against-box collisions and a Verlet rope.
There is one runtime dependency, no framework and no audio files. The sound effects are synthesised with WebAudio. Screen shake, hit-stop (a 30 ms pause on a kill, 200 ms on death) and a pool of 2,000 particles in typed arrays are shared across the games. In headless Chromium at 2× on Wikipedia and MDN, median and 95th-percentile frame times were both 16.7 ms, a steady 60 fps.
Reduced motion is a setting that follows the operating system’s preference, and it turns the shake off. Screen flashes are capped at one per 340 ms, which keeps them under the three-per-second limit in WCAG 2.3.1.
Permissions: three, and no site access
The extension asks for activeTab, scripting and storage. There is no host permission and no content script declared in the manifest, so it installs without the “read and change all your data on all websites” warning and does nothing on any page until you click.
activeTab is the whole trick. Clicking the icon, or pressing the keyboard shortcut, grants temporary access to that one tab. That’s enough to screenshot it and inject the game, and it ends when you leave.
The store build adds one more host permission, api.cybiqon.in, for leaderboards and share links. Both are off until a player turns them on. Leaderboards store the site’s hostname, the game, the score, a nickname and a random player ID. Never the page or its address beyond the hostname. Share links keep a picture of the wrecked page for 30 days. The full list is in the privacy policy.
One packaging detail: the content script is built as a single self-contained file. The default build tool would have split it into a chunk loaded on demand, and loading that chunk into the page needs a host permission we deliberately don’t ask for.
The part that doesn’t flatter us
Our company has a rule, written at the top of the file every session reads: no new product until there is revenue. The failure mode it guards against is starting things.
Page Arcade broke it. It was the fifth time. The decision is recorded in the product’s own notes, with the date and the fact that total earnings that day were $0.03. The scope was supposed to be a local prototype: no store listing, no backend.
It grew in four steps:
- Day one was about 2,100 lines: two games.
- Later that same day, 5,100 lines and three games.
- Two days later, eight games, achievements, skins, a share card and a live backend for leaderboards.
Today it’s 16,809 lines of source and 5,323 of tests. Every expansion was chosen, not drifted into. Each one still made the product bigger, and none of them made it earn.
It earns ₹0. There are no ads, no purchases and no plan to add either. The honest case for it is that it’s the most shareable thing we’ve built: it turns any page into a screenshot worth sending. It’s also a working demonstration of something we sell: reading a web page the way a person does, from its pixels and its structure together. That’s the same skill behind scraping a Google Maps result in MapWit or capturing real footage for our videos.
It still has no kill criterion, a date or a number at which we stop. That is the next decision about it, and it’s overdue.
One more confession. On 22 September we added a cheat for playtesting: type iddqd on the menu and nothing costs a life. The note said to remove it before the store submission. On 4 October the owner decided to keep it. God-mode runs never post to the leaderboards, so it can’t spoil anyone else’s score.
What we’d tell someone building the same thing
- Screenshot first, inject second. Every measurement depends on the page being still, and the only reliable way to keep it still is to stop reading the live page.
- Use pixels and layout together. Either one alone is wrong in ways the other catches.
- Decide which mistake is worse, then bias towards it. For us, a free cell that is really content is worse than a wall that is really empty. Then fix the walls that are really empty anyway, because players notice those.
- Leave the debug view in. The G overlay turned a vague “I died for no reason” into a screenshot with the bug visible in it.
- Measure with scripted play, not by eye. A scripted snake can cross the same line 1,456 times at three zoom levels; a person playing tests a handful. We did the same for our puzzle games, where eight bots played every level.
Page Arcade is free on the Chrome Web Store. Try it on the page you’re reading.
FAQ
How do you turn a website into a game?
Page Arcade, a free Chrome extension, takes a screenshot of the page and asks the browser where every line of text and every image is drawn. It then lays a game over the frozen page. The page’s empty space becomes Snake’s playing field, and its words become bricks, targets and falling enemies. Pressing Esc removes the game and leaves the page exactly as it was.
How does Page Arcade decide where the empty space on a page is?
Page Arcade cuts the page into 12-pixel cells. A cell is a wall if its screenshot pixels vary by more than 24 in any colour channel, or if the browser reports text or media in it. Thin lines of 3 pixels or less, such as <hr> dividers and borders, are classified separately and can be crossed. Text, icons and buttons never can.
Does Page Arcade read or change my data on websites?
No. Page Arcade asks for activeTab, scripting and storage, with no host permission. It does nothing on a page until you click its icon or press Alt+Shift+P, and it never edits the page. Leaderboards and share links are opt-in, and they send only the site’s hostname, your score and a nickname, or the share picture you chose to upload.
What games are in Page Arcade?
Page Arcade has eight games: Snake, Word Blaster, Type Storm, Brickout, Whack-a-Word, Page Roller, Gravity Crumble and Page Chomp. Version 1.1.0, released on 22 October 2026, added new modes, power-ups and bosses to Snake, Word Blaster, Type Storm and Brickout, plus a redesigned menu and HUD.
Is there a cheat code in Page Arcade?
Yes. Typing iddqd on the Page Arcade menu turns on god mode for the session, so nothing costs a life. It was added for playtesting and kept on purpose. God-mode runs still earn local records and XP, but they never post to the leaderboards.
Sources
- chrome.tabs.captureVisibleTab — Chrome for Developers
- The activeTab permission — Chrome for Developers
- Range.getClientRects() — MDN
- Kick Ass — the 2010 Asteroids-on-any-page bookmarklet
- Katamari Hack — rolling up a page’s words, 2011
- Google Gravity — the Google homepage in a physics engine
- PlayAnyPage — a 2026 bookmarklet with Brickout and Snake on any page
- ZType — the typing shooter Type Storm follows
- Planck.js — the 2D physics library behind Gravity Crumble
- Understanding WCAG 2.3.1: Three Flashes or Below Threshold — W3C
- Juicing Your Cameras With Math — Squirrel Eiserloh, GDC 2016, on trauma-based screen shake
Chrome ExtensionsEngineeringGames
Read next
- Our Chrome extension sat untouched for four months. Review took one day.MapWit is a free Chrome extension for Google Maps lead generation: it scores the local businesses in a search and exports them to Excel, with no server and no account. It sat untouched for four months, went from zero tests to 171 in one day, and passed Chrome Web Store review on the first round.
- Eight bots played all 500 levels. They were measuring the wrong game.We built eight bots to play a 500-level arcade game and measure how hard each level is. They played it 720 pixels wide. Phones are 412. Levels do not scale, so the shipped game was roughly twice as hard as the game being measured: 172 of 500 levels in band, not the 421 we were reporting.
- Our puzzle generator lied about difficulty. Twice.Reversing a solved puzzle by K random moves does not give a K-move puzzle. Our first generator produced 14 usable levels from 91,322 candidates; the second, 14 from 16,249. This is the multi-source BFS that finally measured difficulty correctly.