DEV Community

Abdullah Guc
Abdullah Guc

Posted on

I built ~70 tiny browser puzzle games with no framework, no accounts, and a 900-byte score system

Gamebreak is a new site of short browser puzzle games: open one between two tasks, finish in a few minutes, close it. No login wall, no account, no app install. It has around 70 games: Sudoku, Mahjong, FreeCell, 2048, a Tetris-style game, Hashi, Nonogram, Sokoban, a Wordle-style word game and more. Most rounds take 2–4 minutes.

The audience is office workers and students taking short breaks. That one decision drove almost every technical choice below.

No framework, one small script per game

Each game page is plain, hand-written HTML with a handful of script tags. On the 2048 page, for example, the page loads:

  • i18n.js – a tiny language helper (~3 KB)
  • score.js – the score store (~0.9 KB)
  • 2048.js – the game itself (~6 KB)
  • share.js – result sharing
  • ads.js – ad slot setup

That's it. No bundler output, no hydration, no runtime framework. The whole 2048 HTML document is under 10 KB. For a game you open for three minutes, there's no reason to ship more code than the game needs. Shared CSS lives in one site.css, and static assets are served through Cloudflare with a very long max-age, so a second game page mostly costs one HTML request plus its own script.

Writing games without a framework sounds harder than it is. Grid puzzles are mostly a 2D array, a render function, and input handlers. The DOM is plenty fast for a 4×4 or 9×9 grid.

Scores without accounts: localStorage

There is no backend for user data at all. No accounts means no passwords to protect, no personal data to store, and nothing to sign up for. The trade-off is stated right on the page: "The best score stays in this browser."

The entire score system is basically this:

function key(id) {
  return "gamebreak:" + lang + ":" + id;
}

function get(id) {
  try {
    const n = Number(localStorage.getItem(key(id)));
    return Number.isFinite(n) && n > 0 ? n : 0;
  } catch (e) {
    return 0;
  }
}

window.Gamebreak = {
  get,
  setHigh(id, score) { /* keep the max */ },
  setLow(id, score)  { /* keep the min, e.g. fastest time */ },
};
Enter fullscreen mode Exit fullscreen mode

A few small lessons:

  • Wrap everything in try/catch. Some private-browsing modes and locked-down browsers throw on localStorage access. The game should still work; it just won't remember your best.
  • Validate what you read. Number.isFinite guards against garbage values.
  • High vs. low. Score-based games (2048) keep the maximum; time-based puzzles keep the minimum, for example a fastest time. Two helpers cover every game.
  • Namespace your keys. Keys look like gamebreak:en:2048, so they never collide with anything else on the origin.

Turkish first, then English and Spanish

Turkish is the default: Turkish pages live at the root (/game/2048/), English under /en/games/, Spanish under /es/juegos/. The slugs are translated too, so each audience gets URLs in its own language.

For SEO, every page declares its siblings with hreflang plus an x-default:

<link rel="alternate" hreflang="tr" href="https://gamebreak.lol/game/2048/">
<link rel="alternate" hreflang="en" href="https://gamebreak.lol/en/games/2048/">
<link rel="alternate" hreflang="es" href="https://gamebreak.lol/es/juegos/2048/">
<link rel="alternate" hreflang="x-default" href="https://gamebreak.lol/game/2048/">
<link rel="canonical" href="https://gamebreak.lol/en/games/2048/">
Enter fullscreen mode Exit fullscreen mode

Each page also has its own description, Open Graph locale (en_US with tr_TR and es_ES alternates), and an entry in the sitemap. Because pages are real HTML documents with real text, crawlers don't need to execute JavaScript to understand what a page is about.

In-game strings (like "round over" or "no moves left") are handled by a tiny helper:

GB.msg("Tur bitti", "Round over", "Ronda terminada");
Enter fullscreen mode Exit fullscreen mode

It reads <html lang> and returns the right string, falling back to English if a Spanish string is missing. It's not a "proper" i18n library, but with three languages and short UI strings, it's enough.

Designed for a break, not for engagement

The product decisions mattered more than the code:

  • Short rounds. Most games finish in 2–4 minutes.
  • Plain rules at the top. The 2048 description is literally: "Slide matching numbers together. Two twos make a four. Reach 2048." No tutorial screens.
  • Nothing to sign up for. You open the page and play.

Honest trade-offs

  • Scores don't sync across devices. That's the price of no accounts.
  • The site is free and supported by ads served through Google AdSense, including auto ads, and it uses Google Analytics. That's the cost of keeping it free without accounts or paywalls.
  • Hand-rolled i18n and per-game scripts mean some duplication across ~70 games, in exchange for pages that stay tiny and independent.

If you want to try it on your next break, start with 2048 or browse the full list at gamebreak.lol. Feedback is very welcome – especially on games that take longer than they should, or anything that breaks on your device.

Top comments (0)