<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdullah Guc</title>
    <description>The latest articles on DEV Community by Abdullah Guc (@abdullahguch).</description>
    <link>https://dev.to/abdullahguch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4219124%2F1bf5e5f0-879c-466c-b492-a666e6daf827.jpg</url>
      <title>DEV Community: Abdullah Guc</title>
      <link>https://dev.to/abdullahguch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdullahguch"/>
    <language>en</language>
    <item>
      <title>I built ~70 tiny browser puzzle games with no framework, no accounts, and a 900-byte score system</title>
      <dc:creator>Abdullah Guc</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:08:03 +0000</pubDate>
      <link>https://dev.to/abdullahguch/i-built-70-tiny-browser-puzzle-games-with-no-framework-no-accounts-and-a-900-byte-score-system-1mn1</link>
      <guid>https://dev.to/abdullahguch/i-built-70-tiny-browser-puzzle-games-with-no-framework-no-accounts-and-a-900-byte-score-system-1mn1</guid>
      <description>&lt;p&gt;&lt;a href="https://gamebreak.lol" rel="noopener noreferrer"&gt;Gamebreak&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;The audience is office workers and students taking short breaks. That one decision drove almost every technical choice below.&lt;/p&gt;

&lt;h2&gt;
  
  
  No framework, one small script per game
&lt;/h2&gt;

&lt;p&gt;Each game page is plain, hand-written HTML with a handful of script tags. On the &lt;a href="https://gamebreak.lol/en/games/2048/" rel="noopener noreferrer"&gt;2048 page&lt;/a&gt;, for example, the page loads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;i18n.js&lt;/code&gt; – a tiny language helper (~3 KB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;score.js&lt;/code&gt; – the score store (~0.9 KB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2048.js&lt;/code&gt; – the game itself (~6 KB)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;share.js&lt;/code&gt; – result sharing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ads.js&lt;/code&gt; – ad slot setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;code&gt;site.css&lt;/code&gt;, and static assets are served through Cloudflare with a very long &lt;code&gt;max-age&lt;/code&gt;, so a second game page mostly costs one HTML request plus its own script.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scores without accounts: localStorage
&lt;/h2&gt;

&lt;p&gt;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: &lt;em&gt;"The best score stays in this browser."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The entire score system is basically this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gamebreak:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Gamebreak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;setHigh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* keep the max */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;setLow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* keep the min, e.g. fastest time */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few small lessons:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Turkish first, then English and Spanish
&lt;/h2&gt;

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

&lt;p&gt;For SEO, every page declares its siblings with &lt;code&gt;hreflang&lt;/code&gt; plus an &lt;code&gt;x-default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"tr"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://gamebreak.lol/game/2048/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://gamebreak.lol/en/games/2048/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://gamebreak.lol/es/juegos/2048/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://gamebreak.lol/game/2048/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://gamebreak.lol/en/games/2048/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page also has its own description, Open Graph locale (&lt;code&gt;en_US&lt;/code&gt; with &lt;code&gt;tr_TR&lt;/code&gt; and &lt;code&gt;es_ES&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;In-game strings (like "round over" or "no moves left") are handled by a tiny helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tur bitti&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Round over&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ronda terminada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads &lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designed for a break, not for engagement
&lt;/h2&gt;

&lt;p&gt;The product decisions mattered more than the code:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Honest trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scores don't sync across devices. That's the price of no accounts.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Hand-rolled i18n and per-game scripts mean some duplication across ~70 games, in exchange for pages that stay tiny and independent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try it on your next break, start with &lt;a href="https://gamebreak.lol/en/games/2048/" rel="noopener noreferrer"&gt;2048&lt;/a&gt; or browse the full list at &lt;a href="https://gamebreak.lol" rel="noopener noreferrer"&gt;gamebreak.lol&lt;/a&gt;. Feedback is very welcome – especially on games that take longer than they should, or anything that breaks on your device.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
