<?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: Dumb Game Lab</title>
    <description>The latest articles on DEV Community by Dumb Game Lab (@dumbgamelab).</description>
    <link>https://dev.to/dumbgamelab</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%2F4205700%2F66e40244-847a-4f50-82f9-d4bb04363bf9.png</url>
      <title>DEV Community: Dumb Game Lab</title>
      <link>https://dev.to/dumbgamelab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dumbgamelab"/>
    <language>en</language>
    <item>
      <title>My fortune app made 4 won in two weeks and I patched it three times anyway</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Sat, 14 Nov 2026 02:12:13 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/my-fortune-app-made-4-won-in-two-weeks-and-i-patched-it-three-times-anyway-4bim</link>
      <guid>https://dev.to/dumbgamelab/my-fortune-app-made-4-won-in-two-weeks-and-i-patched-it-three-times-anyway-4bim</guid>
      <description>&lt;p&gt;4 won. That's about $0.003, from 4 ad impressions, for the first two weeks of a small fortune app I shipped on October 16&lt;/p&gt;

&lt;p&gt;The app does Saju animal compatibility inside Toss. Toss is a big Korean finance app, and other people can run small apps inside it as mini apps. Saju is the Korean four pillars reading. Your birth date and hour become pillars, and each day pillar maps to an animal and an element. There are 60 day pillars, and I wrote a reading for every one of them before launch&lt;/p&gt;

&lt;p&gt;I also went deep on the calendar side. The Saju year turns over at the exact minute of Ipchun, the start of spring, so early February birthdays can land in the previous year. Korea ran daylight saving time in some years, which shifts birth hours from those summers. Then there's true solar time, because the clock in Seoul and the sun over Seoul don't agree. All of that is in there&lt;/p&gt;

&lt;p&gt;Six people used it in two weeks&lt;/p&gt;

&lt;p&gt;All six were us, testing&lt;/p&gt;

&lt;p&gt;Real users: 0. For five days in a row daily actives were 0 and I was still opening the code every morning&lt;/p&gt;

&lt;p&gt;I pushed three updates after launch. Only 30% of the people who started the input screen got to the end, so I made "I don't know my birth time" the default. Then I took out the greyed-out submit button and put in a message that points at the field you missed&lt;/p&gt;

&lt;p&gt;Both fixes are fine. Neither one could move anything. 30% of six testers isn't a measurement, and with zero real users there was nobody to convert. I kept working on the input screen because it was the part I could edit. Nobody arriving had no file I could open&lt;/p&gt;

&lt;p&gt;Same month, we put a jjimjilbang idle game into Toss too. First day there was also 0 users. As far as I can tell Toss doesn't give mini apps much organic exposure. You get listed and that's roughly it&lt;/p&gt;

&lt;p&gt;Before launch I'd set a rule: under 10,000 won in two weeks and it stops. It cleared that easily, in the wrong direction. On October 31 I called it. The app is still up, I just stopped working on it&lt;/p&gt;

&lt;p&gt;Now, before I touch code on anything live, I count new users from the last 7 days. If it's 0 I close the editor&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dumbgamelab.com/?utm_source=launch-devto&amp;amp;utm_medium=community" rel="noopener noreferrer"&gt;https://dumbgamelab.com/?utm_source=launch-devto&amp;amp;utm_medium=community&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>mobile</category>
    </item>
    <item>
      <title>One 5-second alarm in my pixel game ate the whole account's free Durable Objects reads</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Sun, 08 Nov 2026 00:29:04 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/one-5-second-alarm-in-my-pixel-game-ate-the-whole-accounts-free-durable-objects-reads-410l</link>
      <guid>https://dev.to/dumbgamelab/one-5-second-alarm-in-my-pixel-game-ate-the-whole-accounts-free-durable-objects-reads-410l</guid>
      <description>&lt;p&gt;5 million rows read. That's the daily free limit for SQLite-backed Durable Objects, and on 11-07 we went past it&lt;/p&gt;

&lt;p&gt;The limit is per account. That's the part I hadn't really thought about&lt;/p&gt;

&lt;p&gt;What burned through it was one alarm. We run a small multiplayer pixel game called Dothyeolmaeng, and one of its Durable Objects sets an alarm that fires every 5 seconds. Every time it fired it ran a query on a table with no index on the column it filtered by, so SQLite scanned the whole table. Every 5 seconds, all day.&lt;/p&gt;

&lt;p&gt;On its own that's just a slow query. The bad part is that the same Cloudflare account also runs our error collector and the server that handles friend invites. When the alarm used up the shared read budget, both of them stopped too. So the thing that was supposed to tell me something broke was also broken&lt;/p&gt;

&lt;p&gt;The fix was mostly boring. I added two indexes so the alarm reads the rows it needs and skips the rest of the table. The alarm was also reading the bot list from the table on every tick, and that list barely changes, so now it's cached in memory and only reloaded when it actually changes.&lt;/p&gt;

&lt;p&gt;Then I added a watcher that checks usage every hour, so the next time something like this starts climbing I see it the same day and not when everything goes down&lt;/p&gt;

&lt;p&gt;If you're on DO SQLite, here's what you can check in five minutes. The cursor from &lt;code&gt;sql.exec&lt;/code&gt; has a &lt;code&gt;rowsRead&lt;/code&gt; field. Log it next to the query:&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;const&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowsRead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it after you've gone through the rows, since it fills in as the cursor is read. Then look for any query where rowsRead is way bigger than the number of rows you actually got back. That gap is the full table scan. Ours read the whole table to get back a handful of rows, on a 5 second loop&lt;/p&gt;

&lt;p&gt;I'd been staring at request counts on the dashboard and they looked completely fine. Requests were never the issue, rows were&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dumbgamelab.com" rel="noopener noreferrer"&gt;https://dumbgamelab.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>cloudflare</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Our PR checks took 3.5 minutes and nobody timed it for weeks</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Mon, 02 Nov 2026 11:20:51 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/our-pr-checks-took-35-minutes-and-nobody-timed-it-for-weeks-386</link>
      <guid>https://dev.to/dumbgamelab/our-pr-checks-took-35-minutes-and-nobody-timed-it-for-weeks-386</guid>
      <description>&lt;p&gt;We keep a few mobile games in one repo. Over time the fast CI job turned into 88 small check scripts that ran one after another. File size limits, missing translation strings, colors outside the palette, scene hierarchy rules, that kind of thing. Each one finishes in a few seconds, so nobody thought of any of them as slow&lt;/p&gt;

&lt;p&gt;All together, every PR sat there for about 3.5 minutes before it could merge.&lt;/p&gt;

&lt;p&gt;We lived with that for weeks. I didn't time it once. What finally did it was the owner of the studio complaining about CI being the bottleneck twice in the same morning, and the second time I opened the job log and actually added up the step times&lt;/p&gt;

&lt;p&gt;The fix was boring. None of the 88 scripts depend on each other, so I put them behind a small parallel runner that starts them all at once and collects exit codes. Auto-merge used to be its own job that waited for the check job, booted a fresh runner and checked out the repo again. I moved it into the last step of the same job, which got rid of that second startup.&lt;/p&gt;

&lt;p&gt;Now it's about 1 minute per PR&lt;/p&gt;

&lt;p&gt;The annoying part is how long 3.5 minutes felt normal. You push, go get water, come back, it's merged. Then one day you push ten small PRs and you've spent half an hour watching a spinner&lt;/p&gt;

&lt;p&gt;One of the games in that repo is an idle internet cafe tycoon for phones, if you're curious what all those palette checks are guarding&lt;br&gt;
&lt;a href="https://qqq1212341.github.io/pcbang-alba-sim/?lang=en&amp;amp;utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=launch-devto" rel="noopener noreferrer"&gt;https://qqq1212341.github.io/pcbang-alba-sim/?lang=en&amp;amp;utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=launch-devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ci</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Three fixes in 43 minutes for a 2GB memory warning, and only the third one mattered</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Sat, 31 Oct 2026 07:10:52 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/three-fixes-in-43-minutes-for-a-2gb-memory-warning-and-only-the-third-one-mattered-11pk</link>
      <guid>https://dev.to/dumbgamelab/three-fixes-in-43-minutes-for-a-2gb-memory-warning-and-only-the-third-one-mattered-11pk</guid>
      <description>&lt;p&gt;We recently put our browser game One Life on the Playgama web portal. It's a small game where each year of your life is one line, and the start screen has a globe drawn on a canvas.&lt;/p&gt;

&lt;p&gt;Playgama runs an analyzer on every upload. Ours came back with a warning that the page was using over 2GB of memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three pull requests
&lt;/h2&gt;

&lt;p&gt;11:10. We blamed the globe first, moved its canvas onto its own compositing layer and shipped it.&lt;/p&gt;

&lt;p&gt;11:21. It still looked like a rendering problem to us, so we switched the globe to a software canvas and shipped again.&lt;/p&gt;

&lt;p&gt;Neither fix touched the logo, which was still sitting on top of the globe canvas.&lt;/p&gt;

&lt;p&gt;11:53. We took the logo out of the layout flow with &lt;code&gt;position: absolute&lt;/code&gt;. The overlap went away, and the warning went with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually happening
&lt;/h2&gt;

&lt;p&gt;The analyzer loads the page in headless Chrome with no GPU. With the logo on top of the globe canvas, the main thread stalled on first paint and memory went past 2GB. It took both elements to trigger it. Removing only the logo made the problem disappear, and so did removing only the globe.&lt;/p&gt;

&lt;p&gt;That means the layer change and the software canvas were both tweaks to the rendering of something that was only half the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part we're not proud of
&lt;/h2&gt;

&lt;p&gt;Every time, we shipped the next fix before the analyzer had finished reporting on the last one. Three PRs in 43 minutes sounds quick. Really it means we never let one attempt tell us anything before we moved on.&lt;/p&gt;

&lt;p&gt;Our first commit message said we'd bisect with variant uploads: one build without the logo, one without the globe, then compare. That would have pointed straight at the overlap. We wrote it down in the first commit. Then we skipped it.&lt;/p&gt;

&lt;p&gt;If a warning only shows up in a headless run, check what overlaps what before you touch any rendering flags.&lt;/p&gt;

&lt;p&gt;The game, if you want to see the globe: &lt;a href="https://qqq1212341.github.io/one-life/?utm_source=launch-devto&amp;amp;utm_medium=community" rel="noopener noreferrer"&gt;https://qqq1212341.github.io/one-life/?utm_source=launch-devto&amp;amp;utm_medium=community&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>9 days ago I launched a game where every year of your life is one line. 8,589 choices, 0 visits back from shares</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:29:37 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/9-days-ago-i-launched-a-game-where-every-year-of-your-life-is-one-line-8589-choices-0-visits-2112</link>
      <guid>https://dev.to/dumbgamelab/9-days-ago-i-launched-a-game-where-every-year-of-your-life-is-one-line-8589-choices-0-visits-2112</guid>
      <description>&lt;p&gt;One Life is a free browser life sim: you get one line of story per year, and your birthplace is randomly weighted using UN birth data for 124 countries.&lt;/p&gt;

&lt;p&gt;Nine days after launch, &lt;a href="https://dumbgamelab.com/play/one-life/?utm_source=devto" rel="noopener noreferrer"&gt;One Life&lt;/a&gt; has had 263 players start 789 lives. They made 8,589 choices at crossroads and reached 181 deaths.&lt;/p&gt;

&lt;p&gt;The sharing numbers looked less dead at first. Players tapped the birth-card share button 88 times across 29 people, and saved the card image 24 times across 14 people.&lt;/p&gt;

&lt;p&gt;Visits arriving through a share link: zero.&lt;/p&gt;

&lt;p&gt;My current explanation is painfully ordinary. The card is an image. Saving or posting it does not carry the URL along. My share text did contain a link, but most shares were the image, so the useful part was easy to lose.&lt;/p&gt;

&lt;p&gt;I am changing the output rather than asking players to behave differently. The game will write a result sentence containing a number, a title, and an everyday unit, with the link inside that text. A Japanese browser game that went viral this week used the same basic pattern: its share button produced result text like 「永」銀賞 74点 plus a link.&lt;/p&gt;

&lt;p&gt;The image can still travel. Now the sentence can travel without it.&lt;/p&gt;

&lt;p&gt;When players share a screenshot, how do you make sure the link survives?&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>analytics</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Our crash reporter's first day: three web standards that killed boot on old Android WebViews</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Tue, 27 Oct 2026 01:06:24 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/our-crash-reporters-first-day-three-web-standards-that-killed-boot-on-old-android-webviews-2a9d</link>
      <guid>https://dev.to/dumbgamelab/our-crash-reporters-first-day-three-web-standards-that-killed-boot-on-old-android-webviews-2a9d</guid>
      <description>&lt;p&gt;We run about a dozen small HTML5 games wrapped as Android apps. Last week we finally put a crash reporter in all of them: a tiny script at the very top of &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; that sends any uncaught error to our own worker.&lt;/p&gt;

&lt;p&gt;The first day of data had a pattern we didn't expect. The top boot crashes weren't bugs in our game code. They were standard functions that simply don't exist on older Android WebViews:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CanvasRenderingContext2D.roundRect&lt;/code&gt; — killed boot in our claw machine game. We use it for rounded buttons and the share card.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Array.prototype.at&lt;/code&gt; — &lt;code&gt;arr.at(-1)&lt;/code&gt; is so convenient that it crept into a dozen places.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;structuredClone&lt;/code&gt; — used to copy save data in two tycoon games.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three work on any phone from the last couple of years, and that's why our own testing never caught them. Players on older devices (still around Chrome 90 inside the WebView) got a white screen and left. No review, no report. The reporter was the first time we saw them at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did
&lt;/h2&gt;

&lt;p&gt;We didn't patch each game. We wrote one small compat file that gets injected in the same spot as the crash reporter, before any game script runs:&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;var&lt;/span&gt; &lt;span class="nx"&gt;def&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;configurable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;at&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trunc&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;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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="o"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;def&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;structuredClone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&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;structuredClone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// roundRect: moveTo/lineTo/arcTo with radii clamped to half the shorter side,&lt;/span&gt;
&lt;span class="c1"&gt;// installed on CanvasRenderingContext2D, OffscreenCanvasRenderingContext2D and Path2D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;structuredClone&lt;/code&gt; fallback is lossy (no Maps, Dates or cycles), but our save data is plain JSON anyway, so it's fine for us. Check yours before copying it.&lt;/p&gt;

&lt;p&gt;Rules we set for ourselves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the reporter shows "X is not a function" and X is a web standard, the fix goes in the compat file, not in the game. The next game gets it for free.&lt;/li&gt;
&lt;li&gt;There's a self-test that creates a canvas context with &lt;code&gt;roundRect&lt;/code&gt; removed and checks that the shim fills it in. It runs with the rest of our checks.&lt;/li&gt;
&lt;li&gt;The shim only ships in store builds. The dev server doesn't load it, so we don't hide the problem from ourselves while developing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The claw machine game got a hotfix the same day (1.0.4) and the boot errors from that version stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you ship web games inside a WebView, your minimum browser is whatever the oldest phone in your player base has, not your own phone. A crash reporter at the top of &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; is about 2 KB and pays for itself on day one.&lt;/p&gt;

&lt;p&gt;If you want to see the claw machine game that started this: &lt;a href="https://play.google.com/store/apps/details?id=com.dumbgamelab.bangguseokclaw" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.dumbgamelab.bangguseokclaw&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>android</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Listing browser games on a Taiwanese web game hub: what the auto-check looks for</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:13:46 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/listing-browser-games-on-a-taiwanese-web-game-hub-what-the-auto-check-looks-for-430f</link>
      <guid>https://dev.to/dumbgamelab/listing-browser-games-on-a-taiwanese-web-game-hub-what-the-auto-check-looks-for-430f</guid>
      <description>&lt;p&gt;I make small free mobile and browser games as a one-person studio in Korea. My numbers keep pointing at Taiwan: in the last 28 days, my internet cafe tycoon got 501 players from Taiwan and 434 from Korea. So today I tried 4388.fun, a Taiwanese site that collects free browser games. When I checked it listed about 4.9k games, with 29 new ones in the past 24 hours.&lt;/p&gt;

&lt;p&gt;There is no review queue. You sign in with Google, paste a game URL, and an automatic check decides on the spot. If the game passes, it is listed right away. You can submit up to 3 games a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check looks at
&lt;/h2&gt;

&lt;p&gt;From the submit page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs in the browser. No downloads, app store pages, social posts or source-code pages.&lt;/li&gt;
&lt;li&gt;It uses HTTPS, and the domain is not on Cloudflare's malware and phishing blocklist.&lt;/li&gt;
&lt;li&gt;It is not already listed.&lt;/li&gt;
&lt;li&gt;It is a single game, not a portal's home or category page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned from three submissions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The one-line description matters.&lt;/strong&gt; One of my games was rejected as not looking like a game. I added "quiz game" to the one-line description and it passed on the next try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload your own cover.&lt;/strong&gt; Without one, the site tries to grab the cover from your game page. I used a store screenshot that was already in Traditional Chinese.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the line in the visitors' language.&lt;/strong&gt; For the tycoon I reused the Traditional Chinese line from its store listing instead of writing a new English one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your own domain is fine.&lt;/strong&gt; All three games are on my own site, not on a portal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three games
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One Life, a life sim where you are born by real birth odds: &lt;a href="https://4388.fun/g/5718" rel="noopener noreferrer"&gt;https://4388.fun/g/5718&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Adult vs Kid Inside, a 1-minute quiz that tells you how much of you is still a kid: &lt;a href="https://4388.fun/g/5719" rel="noopener noreferrer"&gt;https://4388.fun/g/5719&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Internet Cafe: Idle Tycoon: &lt;a href="https://4388.fun/g/5973" rel="noopener noreferrer"&gt;https://4388.fun/g/5973&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will check the play counts later this week and post the numbers. If you have a small browser game with players in Taiwan, this one costs five minutes.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Our feedback box's busiest tester turned out to be Google's test robot</title>
      <dc:creator>Dumb Game Lab</dc:creator>
      <pubDate>Sun, 25 Oct 2026 02:08:19 +0000</pubDate>
      <link>https://dev.to/dumbgamelab/our-feedback-boxs-busiest-tester-turned-out-to-be-googles-test-robot-4k89</link>
      <guid>https://dev.to/dumbgamelab/our-feedback-boxs-busiest-tester-turned-out-to-be-googles-test-robot-4k89</guid>
      <description>&lt;p&gt;We put a feedback button in every build we ship. Tap it, type a line, and the report arrives with the device model and the app version. Each report becomes a ticket, and each ticket starts a fix attempt.&lt;/p&gt;

&lt;p&gt;Then a trivia app we had just uploaded to Play got seven reports in a single day. The texts were &lt;code&gt;ospuuq&lt;/code&gt;, &lt;code&gt;pzxoim&lt;/code&gt;, &lt;code&gt;caokah&lt;/code&gt;. Six lowercase letters each. All seven came from the same OnePlus 8 Pro, all on app version 1.0.0.&lt;/p&gt;

&lt;p&gt;It was not a person. It was the Play pre-launch report (Firebase Test Lab's Robo crawler), which taps every input it can find, types something, and presses the button next to it. It does this again on every upload.&lt;/p&gt;

&lt;p&gt;So for a day we had counted a crawler as our most active tester, and some of those tickets had already started fix attempts for a bug that did not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test Lab devices report the system setting &lt;code&gt;firebase.test.lab&lt;/code&gt; as &lt;code&gt;"true"&lt;/code&gt;. A webview cannot read that, so we added a tiny Capacitor plugin that exposes it.&lt;/li&gt;
&lt;li&gt;The feedback widget now sends &lt;code&gt;where: "robot"&lt;/code&gt; from those devices, and the ticket collector skips that value.&lt;/li&gt;
&lt;li&gt;We did not filter by "looks like random letters". That would also throw away the short, odd, real messages from actual players.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The day was lost to believing a number on the dashboard, not to the fix.&lt;/p&gt;

&lt;p&gt;If you ship a free-text field in a mobile build, open your pre-launch report once and read what your first "users" typed into it.&lt;/p&gt;

&lt;p&gt;We make small idle tycoon games, in case you want to see what the robot was testing: &lt;a href="https://qqq1212341.github.io/pcbang-alba-sim/?utm_source=dev-to-gamedev-tag&amp;amp;utm_medium=community" rel="noopener noreferrer"&gt;https://qqq1212341.github.io/pcbang-alba-sim/?utm_source=dev-to-gamedev-tag&amp;amp;utm_medium=community&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>mobile</category>
      <category>debugging</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
