<?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: 99 tools</title>
    <description>The latest articles on DEV Community by 99 tools (@99_tools_dev).</description>
    <link>https://dev.to/99_tools_dev</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%2F4120997%2Fc07fc796-dd63-4f90-b46a-298bcfb152dd.png</url>
      <title>DEV Community: 99 tools</title>
      <link>https://dev.to/99_tools_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/99_tools_dev"/>
    <language>en</language>
    <item>
      <title>The hardest part of automating Mafia was the player who was already dead 🪦</title>
      <dc:creator>99 tools</dc:creator>
      <pubDate>Fri, 18 Sep 2026 08:42:54 +0000</pubDate>
      <link>https://dev.to/99_tools_dev/the-hardest-part-of-automating-mafia-was-the-player-who-was-already-dead-2ab1</link>
      <guid>https://dev.to/99_tools_dev/the-hardest-part-of-automating-mafia-was-the-player-who-was-already-dead-2ab1</guid>
      <description>&lt;p&gt;A game of Mafia usually starts by picking someone who doesn't get to play. They run the night, watch everyone point in the dark, and announce who didn't survive. At a table of five that's a fifth of the room, and it's usually whoever knew the rules well enough to explain them.&lt;/p&gt;

&lt;p&gt;So I built a mode where the phone runs the night instead and everyone plays.&lt;/p&gt;

&lt;p&gt;Then I solved a problem I was very pleased with, and spent rather less time noticing I'd given the answer away somewhere else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Night two. The obvious thing is to check who's still alive and skip the roles whose players are gone. Why wake a Detective who died last night?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because skipping their turn tells the room the Detective is dead.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone is sitting there with their eyes shut. They can't see anything, but they can hear the shape of the night. Last night there was a pause where the Detective did their thing. Tonight there isn't. That silence is free information, handed to every player — including the Mafia, who now know which power role is off the board.&lt;/p&gt;

&lt;p&gt;Human moderators have long known this. You call the dead role anyway. You pause as though someone is deciding. Then you tell them to close their eyes.&lt;/p&gt;

&lt;p&gt;So the machine does too:&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;detective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detective&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;naming&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;alive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;livingWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detective&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&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;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;detective-pick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detective&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, tap the number of the person you are looking at tonight.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;alive&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seats&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;hold&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="c1"&gt;// stop, wait for a tap&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;THINKING&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;                             &lt;span class="c1"&gt;// 6.5s, then move on&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same words either way. With your eyes shut, a night where the Detective is dead sounds exactly like one where they aren't. I was pleased with this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I missed
&lt;/h2&gt;

&lt;p&gt;Then I opened my own tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Ada did not survive the night. **They were a Doctor.&lt;/em&gt;&lt;em&gt;"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reveal-on-death. It's the default I shipped, and it announces in broad daylight the exact fact the night code works so hard to hide. Every careful pause I'd built was guarding a secret the app had already read out to the room.&lt;/p&gt;

&lt;p&gt;The precaution isn't wrong. It's conditional. It only does anything when you turn &lt;em&gt;"say what they were"&lt;/em&gt; off — the harder game, and not the one the tool hands you by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;Not "silence is information", though it is.&lt;/p&gt;

&lt;p&gt;The thing worth taking away is that &lt;strong&gt;an information leak is a property of the whole system, not of the function you happen to be looking at.&lt;/strong&gt; My night code was careful. My morning code gave the same fact away for free — 115 lines further down the same file, in the same commit, written the same afternoon. Both were correct on their own, and I still didn't see it. You can't review a leak function by function. You have to ask what a player knows by the end of the round, and work backwards.&lt;/p&gt;

&lt;p&gt;I've kept the precaution in, because it's never wrong. But I'd assumed it was nearly free, and when I actually added up the steps around it — wake the role, ask the question, hold the silence, send them back to sleep — a dead Detective costs &lt;strong&gt;22 seconds a night&lt;/strong&gt;, and a dead Doctor 18. With both gone that's &lt;strong&gt;40 seconds of dead air, every single night&lt;/strong&gt;, spent convincing a room that two people who aren't there are still thinking.&lt;/p&gt;

&lt;p&gt;Which makes the default it's guarding look much worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, a question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should role reveal be on by default?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On, and the game is easy to follow — you know what's left, the day's argument has real information in it, and a first-timer can keep up. Off, and every timing precaution suddenly earns its keep, deduction gets genuinely hard, and the game is better for a table that's played before.&lt;/p&gt;

&lt;p&gt;Right now it's on, because it's friendlier — which means anyone on the defaults is paying up to 40 seconds a night to hide something the app announced at breakfast. I'm not sure that's the correct call. If your table has a strong opinion, I'd like to hear it.&lt;/p&gt;

&lt;p&gt;And a broader one: &lt;strong&gt;what else should be digitised?&lt;/strong&gt; This started as a role dealer and became a narrator because it turned out the machine could do the boring half of the job. Other games have that shape — Coup, Avalon, Wink Murder, anything where one person ends up as the admin instead of a player. If that's a game your group plays, tell me and I'll have a go.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://99tools.dev/tools/role-dealer" rel="noopener noreferrer"&gt;99tools.dev/tools/role-dealer&lt;/a&gt;&lt;/strong&gt; — set the narrator to &lt;strong&gt;Godless&lt;/strong&gt;, deal, pass the phone round, and it runs the night. Free, no sign-up, entirely in your browser.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Two bugs that shipped 114 tools' JavaScript to every page</title>
      <dc:creator>99 tools</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:36:02 +0000</pubDate>
      <link>https://dev.to/99_tools_dev/two-bugs-that-shipped-114-tools-javascript-to-every-page-3ef5</link>
      <guid>https://dev.to/99_tools_dev/two-bugs-that-shipped-114-tools-javascript-to-every-page-3ef5</guid>
      <description>&lt;p&gt;I've been building a set of browser-based developer utilities — JSON formatting, diffing, hashing, image and PDF work, that sort of thing. 114 of them now, all on one Next.js static export.&lt;/p&gt;

&lt;p&gt;Two problems took far longer to understand than to fix. Both are the kind that don't announce themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every page shipped every tool's code
&lt;/h2&gt;

&lt;p&gt;The site has a registry: one module listing every tool, its metadata, and its React component. The header imports it to build the search palette. That seemed harmless.&lt;/p&gt;

&lt;p&gt;It wasn't. Here's the rule I didn't know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the App Router, a server module that references a &lt;code&gt;'use client'&lt;/code&gt; module — statically, through &lt;code&gt;next/dynamic&lt;/code&gt;, or through a lazy &lt;code&gt;import()&lt;/code&gt; thunk — has that module's chunk attached to &lt;strong&gt;every route that imports it&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;next/dynamic&lt;/code&gt; felt like it should defer the cost. It doesn't. It defers &lt;em&gt;execution&lt;/em&gt;, not chunk attachment. And because the registry was imported by the layout's header, it was imported by every page — so every page carried every tool's JavaScript, including the PDF library, the OCR engine, and the image processing code.&lt;/p&gt;

&lt;p&gt;A visitor opening a JSON formatter was downloading a barcode scanner.&lt;/p&gt;

&lt;p&gt;The fix was a split by concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the registry holds &lt;strong&gt;data only&lt;/strong&gt; — slugs, names, categories, keywords&lt;/li&gt;
&lt;li&gt;the UI map lives in its own client module&lt;/li&gt;
&lt;li&gt;the prose map is server-only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing clever, but you have to know the rule first. If you want to check your own: &lt;code&gt;ANALYZE=1 next build&lt;/code&gt; emits source maps you can inspect.&lt;/p&gt;

&lt;p&gt;The tell, before I understood the cause, was that every page had suspiciously similar JS weight regardless of what was on it. If your pages all cost the same, something shared is pulling everything in.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A 26.5 MB file I never load broke the entire deploy
&lt;/h2&gt;

&lt;p&gt;The deploy started failing with &lt;code&gt;Asset too large&lt;/code&gt;. The site is static files on Cloudflare, which has a &lt;strong&gt;25 MiB hard limit per asset&lt;/strong&gt; — and exceeding it fails the &lt;em&gt;whole&lt;/em&gt; deploy, not just the page that needs the file. Nothing else ships.&lt;/p&gt;

&lt;p&gt;The culprit was a WebAssembly build I never use.&lt;/p&gt;

&lt;p&gt;I use ONNX Runtime Web for one tool, pinned to the plain WASM backend. But the package also ships a JSEP build — the WebGPU/WebNN variant — and it contains its own &lt;code&gt;new URL(..., import.meta.url)&lt;/code&gt; reference. The bundler traced that reference and emitted a copy of the 26.5 MB file into the output.&lt;/p&gt;

&lt;p&gt;Nothing in my source mentions it. No page requests it. The runtime is explicitly pinned:&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;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/wasm/ort/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionProviders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wasm&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 was emitted purely because a file inside a dependency referenced itself in a way the bundler follows.&lt;/p&gt;

&lt;p&gt;The fix was an ignore file listing that one asset so it isn't uploaded — kept deliberately narrow, because the &lt;em&gt;other&lt;/em&gt; ONNX wasm file in the same folder is the one the tool actually needs. A broad pattern there silently breaks the feature.&lt;/p&gt;

&lt;p&gt;Cloudflare also caps at 20,000 files. Worth knowing before you generate a page per item.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bundlers follow references inside your dependencies, not just yours.&lt;/strong&gt; A file you never import can end up in your output because something in &lt;code&gt;node_modules&lt;/code&gt; pointed at itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static-host limits fail loudly but late.&lt;/strong&gt; Per-asset size and file count are worth checking on day one, not on deploy day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Lazy" in a bundler usually means deferred execution, not deferred download.&lt;/strong&gt; Verify which one you're getting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the build check itself.&lt;/strong&gt; Mine now runs 3,877 assertions over the output directory before deploying, and refuses to ship if any fail. The tool logic has 3,175 tests of its own. Most of both exist because of a specific thing that broke once.&lt;/p&gt;




&lt;p&gt;The tools are at &lt;a href="https://99tools.dev" rel="noopener noreferrer"&gt;99tools.dev&lt;/a&gt; if they're useful to you. Everything runs in the browser — nothing you paste is uploaded, and they keep working with the network disconnected. Two exceptions, badged in the UI: the DNS lookup and the IP viewer genuinely need a server. Analytics is the cookieless kind, and there's nothing to sign up for.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about either bug.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>cloudflare</category>
    </item>
  </channel>
</rss>
