About
How the thing is put together and where to change it.
Files
Eleven files, flat, no subfolders. No build step and no framework. The only thing it fetches from outside is two webfonts. Open index.html straight off your disk or drop the folder on any static host.
index.html home library.html all the tables, as a list or a grid play.html the felt — reads ?g= to know which table proxy.html address bar, four open modes, frame, log youtube.html player, a way out to YouTube, vertical shorts feed link.html link generator settings.html accent, motion, backend, stored data about.html this style.css everything visual, tokens at the top app.js table list, storage, shell, one function per page casino.js the bankroll and all eight games
The header and the sidebar are copied into each HTML file instead of being built by script. That is deliberate. If the JavaScript breaks, you still get a page.
Chips and odds
You start with a thousand chips. There is no way to buy more and no way to take any out. Go broke and the button in the sidebar puts you back to a thousand, as often as you like.
The house edges are the real ones. Roulette keeps 2.70 in every hundred because of the single zero. Dice, crash, mines and hi-lo keep 1.00. Plinko keeps between 0.94 and 1.09 depending on the risk level. The reels keep 3.70. Blackjack sits near 0.50 if you play it well, worse if you do not.
All of those are under a hundred, which is the whole point. Play long enough and the number goes down. That is not a bug in the maths, it is the maths.
Every table prints its return in the top right so you can see what you are up against before you sit down.
Adding a table
Two steps, two files. Write the game in casino.js, then list it in app.js. The id has to be identical in both or the table will not open.
// casino.js
CASINO.games.keno = {
title:'Keno', blurb:'Pick numbers, wait.', rtp:'95.00%',
mount(stage, ctrl, api){
// stage — the middle of the page
// ctrl — your controls, under the bet box
// api — stake, win, record, say, goLabel, onGo, canvas
}
};
// app.js, in TITLES
{ id:'keno', title:'Keno', cat:'Instant',
controls:'Click numbers', rtp:'95.00%', ready:true }
Take a chip off the player with api.stake(), give one back with api.win(n), and log the round with api.record(multiplier). That last one drives the strip of recent results in the sidebar. Set ready:false while you are still working and the row shows up dimmed with a badge instead of disappearing.
Categories build themselves from whatever you type in cat. Invent new ones freely, the filters will follow.
Browse and shorts
The YouTube page has three modes. Watch takes an address and plays it here. Browse hands you off to YouTube itself. Shorts is a vertical feed you scroll with the wheel, a swipe, or the arrow keys, mounting one player at a time so twenty clips do not all load at once.
Browse does not show a grid of videos, and that is deliberate. The only thing a page can fetch without your account is a trending chart, and trending is not the same as recommendations. Your home page is built from your own watch history, which exists solely behind a login, and Google will not let you sign in inside a frame. A grid of strangers' videos labelled "recommended" would have been a lie.
So Browse hands you off to YouTube properly, in a real tab, with shortcuts to your subscriptions, history, watch later and the rest. Sign in there once and everything works. Copy a link back into Watch and it plays here.
Shorts can pull clips on its own if you put a free YouTube Data API key in the sidebar. Without one, paste Shorts links on the Watch tab and they join the feed.
Proxy modes
Four ways to open an address. Three of them share one limitation, so it is worth reading before you pick.
- about:blank window — opens a blank popup and writes a full-height frame into it. The address bar stays on about:blank because the window was never navigated anywhere.
- Blob window — the same document, built in memory and opened from a
blob:address. Unlike about:blank it survives a reload. - Frame — renders on the proxy page itself. Nothing pops up, so nothing can be blocked.
- Plain new tab — an ordinary navigation, no frame anywhere.
The first three put the target inside an iframe. Plenty of sites send X-Frame-Options: DENY or a frame-ancestors policy that forbids exactly that, and when they do, none of the three will load them. Wrapping the frame in a blob or a blank window changes nothing. The browser runs the same check either way.
Two things get past it. A plain tab, because there is no frame to refuse. Or your own server, set in settings, because the page then comes from your origin and those headers never reach the browser. Joetoro appends the encoded address to whatever endpoint you enter.
Links
The main button deploys a second real copy. It reads the eleven files, pushes them to a new GitHub repository, and switches on Pages. What you get back holds its own files and answers on its own address — nothing is redirecting anywhere, and it keeps working if the original goes down. You need a GitHub token with the repo scope; it stays in your browser and is only ever sent to api.github.com.
Uploading a second Cloudflare project by hand gets you the same thing without handing over a token. The free plan allows unlimited projects.
Under those are the throwaway kinds. A blob address only exists inside the tab that made it. Close the tab and it is gone, and it will not open for anyone else.
The portable file is the shareable one. It downloads a small HTML file that opens the launcher when you double-click it. It points at the launcher rather than containing it, so the site still has to be reachable.
Nothing running in a page can mint a genuinely new web address. If you want a second one that works everywhere, deploy the folder again under a different name. Cloudflare Pages allows unlimited projects on the free plan.
Shortcuts
- 1 through 7 — jump between the seven pages
- / — focus the filter box on the library