<?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: MRZHU</title>
    <description>The latest articles on DEV Community by MRZHU (@mrzhu).</description>
    <link>https://dev.to/mrzhu</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%2F3888633%2F78796e15-e1d6-45fb-8d11-c8b0c1385f95.jpg</url>
      <title>DEV Community: MRZHU</title>
      <link>https://dev.to/mrzhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrzhu"/>
    <language>en</language>
    <item>
      <title>A random question button should not ignore the room</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:43:00 +0000</pubDate>
      <link>https://dev.to/mrzhu/a-random-question-button-should-not-ignore-the-room-1ld1</link>
      <guid>https://dev.to/mrzhu/a-random-question-button-should-not-ignore-the-room-1ld1</guid>
      <description>&lt;p&gt;The interesting part of an icebreaker generator is not &lt;code&gt;Math.random()&lt;/code&gt;. It is deciding which questions should reach that line.&lt;/p&gt;

&lt;p&gt;In the current Break The Ice implementation, the homepage picker has three settings: Work, Parties, and Team meetings. There is also an Any group option. This is a small interaction, but it makes the random output less arbitrary. A question about a party playlist belongs in a different pool from a question about a crowded calendar.&lt;/p&gt;

&lt;p&gt;This is a development note about our &lt;a href="https://breaktheice.games/" rel="noopener noreferrer"&gt;Fun Icebreaker Questions generator&lt;/a&gt;, with a few details that are useful for other small random-content tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Select the pool before selecting the item
&lt;/h2&gt;

&lt;p&gt;A category change should immediately produce a question from the new category. It is frustrating to choose Parties and keep looking at the work question from the previous state.&lt;/p&gt;

&lt;p&gt;In React, the change handler passes the new category into the picker function directly. Calling &lt;code&gt;setCategory(nextCategory)&lt;/code&gt; and then relying on the old state value in that same handler would use the previous render's category.&lt;/p&gt;

&lt;p&gt;Here is the important shape, simplified:&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;onCategoryChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextCategory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextCategory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;chooseQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextCategory&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;That explicit argument matters more than adding another animation to the button. The interface has just made a promise about context; the next piece of text needs to keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid the immediate repeat, but describe the guarantee accurately
&lt;/h2&gt;

&lt;p&gt;The picker removes the current question from the candidate array before selecting the next question:&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;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;question&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;choices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&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="nx"&gt;candidates&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextPool&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&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;floor&lt;/span&gt;&lt;span class="p"&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;choices&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents the most visible disappointment: pressing Another question and seeing the same sentence. The fallback also handles a pool containing only one item.&lt;/p&gt;

&lt;p&gt;It does not promise that a question will never repeat during the session. A previous question can return after an intervening draw. For a facilitator working through an entire deck, a shuffled queue with a cursor would offer a different guarantee. For a quick homepage picker, excluding the current item keeps the implementation simple.&lt;/p&gt;

&lt;p&gt;That distinction is worth putting into product language. “Another question” fits this behavior. “Never see a repeat” would not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat copying as an action that can fail
&lt;/h2&gt;

&lt;p&gt;A copy button is useful because the question often travels somewhere else: a meeting agenda, a video-call chat, or a host's notes.&lt;/p&gt;

&lt;p&gt;The current component waits for &lt;code&gt;navigator.clipboard.writeText&lt;/code&gt; before showing Copied. If the clipboard call fails, the page leaves the question visible and asks the person to select it manually. Drawing another question clears both the success and error states.&lt;/p&gt;

&lt;p&gt;Without that reset, a new question can inherit the old question's Copied label. The data changed, but the feedback still describes a previous action. Small tools are full of this kind of state dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context is also a content decision
&lt;/h2&gt;

&lt;p&gt;An office prompt can joke about software or inboxes without asking someone to expose a real conflict. A party prompt can use the snacks and music already in the room. A team prompt can invite a tiny win or a working preference.&lt;/p&gt;

&lt;p&gt;The practical review is to read each question with its selected category visible. Could a person answer in one sentence? Does the question depend on backstory strangers will not have? Can somebody pass without becoming the center of attention?&lt;/p&gt;

&lt;p&gt;These checks are more useful than making the array longer for its own sake. The randomizer can only select what it has been given.&lt;/p&gt;

&lt;p&gt;If you are building a similar tool, try three manual checks: switch categories while watching the displayed question, draw several times to check immediate repeats, and test the clipboard failure state. They cover three different promises: relevance, change, and usable feedback.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Prepared with AI assistance from the project's current implementation. The product link above is our own project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>development</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building fair higher-or-lower clues for a daily football guessing game</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Tue, 08 Sep 2026 01:26:14 +0000</pubDate>
      <link>https://dev.to/mrzhu/building-fair-higher-or-lower-clues-for-a-daily-football-guessing-game-4bf8</link>
      <guid>https://dev.to/mrzhu/building-fair-higher-or-lower-clues-for-a-daily-football-guessing-game-4bf8</guid>
      <description>&lt;p&gt;A daily player-guessing game has a tiny interface, but its feedback model can become surprisingly messy. While working on Wordle Cup, I found that the hardest part was not choosing a mystery player. It was making sure every failed guess taught the player something reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate comparison types
&lt;/h2&gt;

&lt;p&gt;I started by dividing clues into three groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exact categories&lt;/strong&gt; such as nationality, club, league, and position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordered values&lt;/strong&gt; such as age, height, and shirt number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual facts&lt;/strong&gt; that depend on a season or roster snapshot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Exact categories are simple matches. Ordered values need direction as well as color: a miss should say whether the target is higher or lower. Contextual facts need an explicit date, because a club or shirt number can change even when the player record is otherwise correct.&lt;/p&gt;

&lt;p&gt;This distinction removed several awkward special cases from the UI. Instead of every field deciding its own behavior, the clue definition now determines how it is compared and displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize before comparing
&lt;/h2&gt;

&lt;p&gt;User input and roster data rarely use identical strings. Accents, initials, suffixes, and common aliases can make a valid guess look invalid. I treat normalization as a data concern rather than a visual patch.&lt;/p&gt;

&lt;p&gt;A useful normalization pass can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trim and collapse whitespace;&lt;/li&gt;
&lt;li&gt;compare case-insensitively;&lt;/li&gt;
&lt;li&gt;normalize Unicode consistently;&lt;/li&gt;
&lt;li&gt;map known aliases to one player ID;&lt;/li&gt;
&lt;li&gt;keep the displayed name separate from the lookup key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important rule is that normalization helps identify a player; it should not silently merge two different people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make clue direction unambiguous
&lt;/h2&gt;

&lt;p&gt;An arrow only works when the question behind it is obvious. For height, “up” can mean the target is taller. For age, it can mean older. For a shirt number, the direction is numerical but not necessarily meaningful football knowledge.&lt;/p&gt;

&lt;p&gt;I therefore keep the operator attached to the clue definition and pair the arrow with accessible text. Color alone is not enough, especially on a dense mobile grid.&lt;/p&gt;

&lt;p&gt;The comparison result can be modeled as a small, predictable object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ClueResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;miss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;higher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lower&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;That shape is easy to test and keeps rendering logic boring, which is a compliment in a daily game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundaries, not just the happy path
&lt;/h2&gt;

&lt;p&gt;The bugs that matter tend to sit at the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;equal numeric values;&lt;/li&gt;
&lt;li&gt;a player who changed clubs;&lt;/li&gt;
&lt;li&gt;two players with similar names;&lt;/li&gt;
&lt;li&gt;missing height or jersey data;&lt;/li&gt;
&lt;li&gt;a guess that is valid in the database but outside today's pool;&lt;/li&gt;
&lt;li&gt;narrow mobile layouts with long club names.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use fixture players that deliberately trigger each case. A single end-to-end “correct answer” test does not tell us whether the clues remain trustworthy after a roster update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show the data scope
&lt;/h2&gt;

&lt;p&gt;The interface should tell players which roster or season it uses. That short note prevents a large class of reports that are really snapshot disagreements. It also makes maintenance more disciplined because an update changes a visible product contract, not just a hidden data file.&lt;/p&gt;

&lt;p&gt;I have been applying these ideas to &lt;a href="https://wordlecup.today/" rel="noopener noreferrer"&gt;the football guessing games on Wordle Cup&lt;/a&gt;. The biggest lesson so far is that difficulty should come from football knowledge, not from unclear comparison rules. Once each guess produces dependable information, a very small game loop becomes much more satisfying to replay.&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Keeping a 100-question quiz private in the browser</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:40:42 +0000</pubDate>
      <link>https://dev.to/mrzhu/keeping-a-100-question-quiz-private-in-the-browser-1ap</link>
      <guid>https://dev.to/mrzhu/keeping-a-100-question-quiz-private-in-the-browser-1ap</guid>
      <description>&lt;p&gt;A quiz about personal experiences creates a simple product question. How much of the answer data needs to reach a server?&lt;/p&gt;

&lt;p&gt;For a single-session quiz, the useful answer can be none of it.&lt;/p&gt;

&lt;p&gt;The current version of &lt;a href="https://ricepuritytest.party/" rel="noopener noreferrer"&gt;RicePurityTest.party&lt;/a&gt; stores progress in &lt;code&gt;sessionStorage&lt;/code&gt;. A refresh in the same tab can restore the current question and previous yes or no choices. Closing the tab clears that session. There is no account to create and no answer payload to sync.&lt;/p&gt;

&lt;p&gt;The score calculation also stays in the browser. Each checked item subtracts one point from 100. That rule is visible in the site's &lt;a href="https://ricepuritytest.party/methodology" rel="noopener noreferrer"&gt;methodology&lt;/a&gt;, so the result does not depend on a hidden model or personality inference.&lt;/p&gt;

&lt;p&gt;The share flow needs its own privacy boundary. A result card can be useful without becoming a transcript. The card is drawn locally and contains the score, the number of checked items, a result label, an illustration, and the site name. It leaves out the individual questions.&lt;/p&gt;

&lt;p&gt;This design has honest tradeoffs. There is no cross-device resume, no account history, and no way for the site operator to recover a lost session. Those limits are acceptable when the data is sensitive and the product promise is temporary, private use.&lt;/p&gt;

&lt;p&gt;The reusable checklist is short.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep raw answers in browser storage with the narrowest useful lifetime.&lt;/li&gt;
&lt;li&gt;Calculate the result locally when the rules allow it.&lt;/li&gt;
&lt;li&gt;Make the scoring rule inspectable.&lt;/li&gt;
&lt;li&gt;Generate share assets locally and exclude raw answers.&lt;/li&gt;
&lt;li&gt;Explain what hosting and optional analytics can still observe.&lt;/li&gt;
&lt;li&gt;Give the visitor a visible reset path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full &lt;a href="https://ricepuritytest.party/privacy" rel="noopener noreferrer"&gt;privacy explanation&lt;/a&gt; states these boundaries in user-facing language. That page matters as much as the implementation. Privacy claims should describe the system people actually use.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Turned the How to Fish Creature List Into a Search Tool</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:42:28 +0000</pubDate>
      <link>https://dev.to/mrzhu/i-turned-the-how-to-fish-creature-list-into-a-search-tool-2fpd</link>
      <guid>https://dev.to/mrzhu/i-turned-the-how-to-fish-creature-list-into-a-search-tool-2fpd</guid>
      <description>&lt;p&gt;A flat table looked like the obvious way to publish a How to Fish fish list. It also became annoying almost immediately.&lt;/p&gt;

&lt;p&gt;The game has ordinary catches, bosses, quest triggers, five progression stages, several lure tiers, and two rods in the early data. A player usually arrives with one narrow question. Which bait catches Pike? What can the Scientific Lure pull? Is Tuna a normal catch or part of a boss chain?&lt;/p&gt;

&lt;p&gt;Scrolling through a long article makes each of those questions slower than it needs to be. I built a browser-side lookup instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model follows player questions
&lt;/h2&gt;

&lt;p&gt;Each creature record uses a small set of fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Creature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;island&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Regular&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;bait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;note&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;The field named &lt;code&gt;island&lt;/code&gt; needs a careful definition. For regular catches, it represents the stage where the relevant lure tier first appears. It does not claim that every creature is physically locked to one island. Bosses still need the correct encounter area and trigger.&lt;/p&gt;

&lt;p&gt;That distinction belongs in the interface because it changes how a player reads the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  One record can answer several searches
&lt;/h2&gt;

&lt;p&gt;Pike is associated with the forest progression stage, the Fishing Rod, and both the Free Lure and Beginner Lure. A search index can combine those values into one lowercase string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;creature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;creature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;island&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;creature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;creature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bait&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same record now responds to &lt;code&gt;pike&lt;/code&gt;, &lt;code&gt;forest&lt;/code&gt;, &lt;code&gt;fishing rod&lt;/code&gt;, &lt;code&gt;free lure&lt;/code&gt;, and &lt;code&gt;beginner lure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Filters handle the questions that should remain explicit. Island stage, bait, and a bosses-only toggle are easier to understand as controls than as query syntax. The implementation stays small enough to run without a backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bosses should stay in the same index
&lt;/h2&gt;

&lt;p&gt;I considered making a separate creature list for bosses. That would hide useful relationships.&lt;/p&gt;

&lt;p&gt;Tuna is caught with the Professional Boss Lure and then used to trigger the Albatross. The Bowhead Whale comes from the Fish Bucket and later triggers the Mutated Bowhead Whale. A player searching either item should see the whole chain even if its records have different types.&lt;/p&gt;

&lt;p&gt;Keeping everything in one dataset also makes reverse lookups possible. A lure page can group all creatures that include the same bait value. The fish finder and lure lookup become two views of the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty states matter in small tools
&lt;/h2&gt;

&lt;p&gt;A no-result screen should help the player recover. &lt;code&gt;No matches&lt;/code&gt; is technically correct and practically weak.&lt;/p&gt;

&lt;p&gt;The tool suggests clearing the island or bait filter because filter combinations are the common source of an empty result. It also keeps boss searching opt-in. A player looking for an ordinary fish should not have ten encounters mixed into the list by default.&lt;/p&gt;

&lt;p&gt;The current &lt;a href="https://howtofish.ing/fish/" rel="noopener noreferrer"&gt;searchable How to Fish fish list&lt;/a&gt; contains 34 documented creatures and encounters. It can be filtered by progression stage and bait, with bosses available as a separate view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static delivery fits the job
&lt;/h2&gt;

&lt;p&gt;The data changes with game patches, but the query itself does not need a server. Astro generates the page, a short script filters the rendered rows, and every lookup stays in the browser.&lt;/p&gt;

&lt;p&gt;That choice gives the page useful HTML before JavaScript runs. Search engines can read the creature names and relationships, while players still get instant filtering. It also avoids turning a small reference tool into an account product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The difficult part was the wording
&lt;/h2&gt;

&lt;p&gt;Code can filter incorrect data perfectly. The harder work was deciding what each field promised.&lt;/p&gt;

&lt;p&gt;I compared the official Steam material with multiple published catch lists, kept patch-sensitive notes beside the affected records, and avoided inventing drop rates that the sources did not support. The interface says &lt;code&gt;Lure or trigger&lt;/code&gt; because a Carrot and a Scientific Lure do different jobs even though both occupy the bait field.&lt;/p&gt;

&lt;p&gt;Small reference tools earn trust through those distinctions. A fast wrong answer still wastes a cast.&lt;/p&gt;

&lt;p&gt;How to Fish.ing is an independent fan project with no affiliation to Dazed Games or Valve. The dataset described here was checked for Patch 1.0.9 on August 25, 2026.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A stale response can quietly break a recommendation tool</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Sun, 16 Aug 2026 01:23:06 +0000</pubDate>
      <link>https://dev.to/mrzhu/a-stale-response-can-quietly-break-a-recommendation-tool-3p9c</link>
      <guid>https://dev.to/mrzhu/a-stale-response-can-quietly-break-a-recommendation-tool-3p9c</guid>
      <description>&lt;p&gt;A recommendation page can perform the correct calculation and still show the wrong result. The failure happens when two valid requests finish in the wrong order.&lt;/p&gt;

&lt;p&gt;Consider a player who starts a Members search, then switches to F2P while the first request is still running. The F2P request finishes quickly and the page shows F2P methods. A moment later, the slower Members response arrives. If the interface accepts it, Members-only methods replace the correct result even though the form still says F2P.&lt;/p&gt;

&lt;p&gt;Nothing crashed. The API returned valid data twice. The screen became untrustworthy because an older answer won the race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every request an identity
&lt;/h2&gt;

&lt;p&gt;The finder on &lt;a href="https://osrsmoneymakers.com/" rel="noopener noreferrer"&gt;OSRS Money Maker&lt;/a&gt; keeps a counter for recommendation requests. Starting a search increments the counter and stores the new value locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;activeRecommendRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the response returns, the handler compares its local ID with the current counter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;activeRecommendRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="nf"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing a setting also increments the counter and clears the old result. Any response created before that change becomes stale immediately, even if the network request cannot be cancelled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Player lookup needs the same protection
&lt;/h2&gt;

&lt;p&gt;The page can load public Hiscores before it asks for recommendations. That adds another race. A player might type one username, start a lookup, then correct the name before the first lookup finishes.&lt;/p&gt;

&lt;p&gt;The same request-ID pattern protects this path. Only the latest player lookup may update the stored profile or clear the loading state. The recommendation request also checks whether it is still current after waiting for Hiscores. This prevents a corrected username from being paired with the previous account's levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading state can become stale too
&lt;/h2&gt;

&lt;p&gt;Race protection has to cover more than the final data. An old request should not clear the spinner for a newer request. It should not display an old error after a successful retry. It should not scroll the page to results that no longer match the selected settings.&lt;/p&gt;

&lt;p&gt;The current-ID check belongs around every state change that depends on the request. That includes results, error messages, loading flags, and follow-up UI actions.&lt;/p&gt;

&lt;p&gt;AbortController can save bandwidth when the API supports cancellation. The identity check still earns its place. Cancellation can arrive too late, and some intermediate work may already have completed. A small monotonic counter gives the interface a clear rule about which answer owns the screen.&lt;/p&gt;

&lt;p&gt;The calculation can be perfectly accurate for the request it received. The product also has to prove that the response still belongs to the choices visible now.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Why an OSRS Money-Making Method Can Vanish from a Live Recommendation List</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:25:06 +0000</pubDate>
      <link>https://dev.to/mrzhu/why-an-osrs-money-making-method-can-vanish-from-a-live-recommendation-list-3nlk</link>
      <guid>https://dev.to/mrzhu/why-an-osrs-money-making-method-can-vanish-from-a-live-recommendation-list-3nlk</guid>
      <description>&lt;p&gt;A method can be carefully reviewed, remain in a catalog, and still disappear from a live list of recommendations. That is not a data error. In my OSRS money-maker finder, it is often the correct result.&lt;/p&gt;

&lt;p&gt;The catalog and the recommendation list answer different questions. The catalog says that a method has been examined and has usable requirements, item inputs, outputs, and action-rate assumptions. The live list asks whether that method fits this player right now.&lt;/p&gt;

&lt;p&gt;The first test is eligibility. A members-only route should not appear for a free-to-play player. A Wilderness route is excluded when the player says no to Wilderness activity. Skill requirements, starting cash, and the preferred amount of attention can remove other methods.&lt;/p&gt;

&lt;p&gt;The second test is the current market calculation. Inputs are valued at the live high price, outputs at the live low price, and Grand Exchange tax is deducted. If the result is zero or negative, the method is not recommended even though its reviewed record stays in the catalog. The method can return when the prices make sense again.&lt;/p&gt;

&lt;p&gt;Keeping these two layers separate matters. Deleting an unprofitable method would erase useful reviewed information. Showing it as a recommendation would suggest that a historical review overrules the current numbers. Neither choice is honest.&lt;/p&gt;

&lt;p&gt;This is also why the empty state is useful. When &lt;a href="https://osrsmoneymakers.com/" rel="noopener noreferrer"&gt;OSRS Money Makers&lt;/a&gt; finds no match, it means every reviewed option failed at least one declared condition or the current profit test. The player can change a real constraint, such as available cash or Wilderness preference, instead of being handed a filler suggestion.&lt;/p&gt;

&lt;p&gt;The system is still an estimate. Live prices move, orders may fill slowly, and a player may not sustain the reviewed action rate. But a method that vanishes has at least failed a visible rule, not an unexplained score.&lt;/p&gt;

&lt;p&gt;For recommendation software, exclusion logic deserves as much attention as ranking logic. A smaller list with defensible omissions is more useful than a large list that never says no.&lt;/p&gt;

</description>
      <category>gaming</category>
    </item>
    <item>
      <title>I Stopped Treating “Available” as a Boolean</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:22:22 +0000</pubDate>
      <link>https://dev.to/mrzhu/i-stopped-treating-available-as-a-boolean-b4h</link>
      <guid>https://dev.to/mrzhu/i-stopped-treating-available-as-a-boolean-b4h</guid>
      <description>&lt;p&gt;When I started building a status page for a newly released messaging app, I thought the job was simple: answer whether the app was available.&lt;/p&gt;

&lt;p&gt;That assumption lasted about one afternoon.&lt;/p&gt;

&lt;p&gt;“Available” turned out to hide several different states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a store listing exists;&lt;/li&gt;
&lt;li&gt;the listing belongs to the expected publisher;&lt;/li&gt;
&lt;li&gt;the page shows &lt;strong&gt;Install&lt;/strong&gt;, &lt;strong&gt;Pre-register&lt;/strong&gt;, or &lt;strong&gt;Not available&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;the user can create or use the required account;&lt;/li&gt;
&lt;li&gt;the network path works in the user's region;&lt;/li&gt;
&lt;li&gt;the feature has actually rolled out to that account;&lt;/li&gt;
&lt;li&gt;the current build supports the feature being discussed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single boolean could not represent any of this without misleading somebody.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the decision, not the headline
&lt;/h2&gt;

&lt;p&gt;I replaced the boolean with a small set of explicit observations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StoreState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pre_register&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;region_blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;observedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;storeState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StoreState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;accountAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;works&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;untested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;featureRollout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;present&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&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;The important part is not the union type. It is the refusal to collapse different questions into one confident answer.&lt;/p&gt;

&lt;p&gt;For example, a reader in mainland China may be able to see an App Store page while still being unable to finish sign-in. Another reader may install the same build but not receive a feature flag yet. Both experiences are real, and neither is explained by “launched globally.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Put time next to every changing fact
&lt;/h2&gt;

&lt;p&gt;App availability is a cache with a very short half-life. I now attach an observation date to store state, publisher identity, version history, and official help pages. If an article says “there is no standalone app” but a verified publisher releases one later, the old article is not necessarily dishonest; it is simply date-bound.&lt;/p&gt;

&lt;p&gt;This also changed the UI. Instead of a large green “available” badge, the page can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;iOS listing verified on 12 August 2026. Android store state still varies by region. Account access was not independently tested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is less exciting, but much more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate safety evidence from marketing claims
&lt;/h2&gt;

&lt;p&gt;Encryption language creates a similar problem. A product announcement, a store description, and a third-party security review are not equivalent evidence. I keep those layers separate and avoid converting “encrypted” into “safe.” Publisher identity, download source, account recovery, metadata behavior, and implementation review are different checks.&lt;/p&gt;

&lt;p&gt;I keep the live decision pages for this experiment at &lt;a href="https://wexchat.pro/" rel="noopener noreferrer"&gt;WexChat Pro&lt;/a&gt;. It is an independent reference site, not an official X product. The broader engineering lesson applies to any fast-moving product: model what the user must decide, preserve the source and timestamp, and let uncertainty remain visible.&lt;/p&gt;

&lt;p&gt;A status page should not merely report a launch. It should help a real person decide what to do next.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>security</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Which OCR Should I Use for Handwritten Math Answers?</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Tue, 28 Jul 2026 02:17:11 +0000</pubDate>
      <link>https://dev.to/mrzhu/which-ocr-should-i-use-for-handwritten-math-answers-3em5</link>
      <guid>https://dev.to/mrzhu/which-ocr-should-i-use-for-handwritten-math-answers-3em5</guid>
      <description>&lt;p&gt;I am building an English website for a Gaokao math paper. Gaokao is the university entrance exam in China.&lt;/p&gt;

&lt;p&gt;Checking the objective questions is easy. Written questions give me a headache.&lt;/p&gt;

&lt;p&gt;For a written math question, a student may fill half a page with fractions, square roots and small notes. Chinese students write very fast in the exam, so the handwriting can be squeezed together. There may be crossed-out formulas, arrows, or a new step added between two old steps. A geometry question can also have a diagram.&lt;/p&gt;

&lt;p&gt;My first idea was to let the student take a photo and upload it. Then I thought about one simple problem. If OCR reads &lt;code&gt;x + 1&lt;/code&gt; as &lt;code&gt;x - 1&lt;/code&gt;, everything after that is useless. The system may still give a very confident-looking grade.&lt;/p&gt;

&lt;p&gt;This is the part I have not solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools I found
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.mathpix.com/" rel="noopener noreferrer"&gt;Mathpix&lt;/a&gt; looks closest to this problem. It is made for math and other STEM content. It can read images, PDFs and stroke data. I think I will test it first for paper photos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.myscript.com/docs/interactive-ink" rel="noopener noreferrer"&gt;MyScript iink&lt;/a&gt; is a different choice. It uses pen strokes when the user writes on a screen. This may be more accurate, but it also means I need to build a writing canvas and ask the student not to use paper.&lt;/p&gt;

&lt;p&gt;I also checked &lt;a href="https://docs.cloud.google.com/vision/docs/handwriting" rel="noopener noreferrer"&gt;Google Cloud Vision&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/prebuilt/read" rel="noopener noreferrer"&gt;Azure Document Intelligence Read&lt;/a&gt;. They can return handwriting and page layout. Maybe they can handle the Chinese or English notes while another service handles the formulas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.microsoft.com/en-us/research/publication/trocr-transformer-based-optical-character-recognition-with-pre-trained-models/" rel="noopener noreferrer"&gt;TrOCR&lt;/a&gt; is another direction if I want an open-source model. I have not found a clear answer about a whole page with mixed text, formulas and diagrams.&lt;/p&gt;

&lt;p&gt;A vision model may help after OCR. I am careful about this idea. If a line is unclear, I want the system to mark it as unclear. I do not want the model to fill in the line because it knows what a normal solution should look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rough plan
&lt;/h2&gt;

&lt;p&gt;I will first reject photos that are blurred, cropped or badly tilted. After that, I may use one recognizer for formulas and another one for normal handwriting. I also want to keep the position and confidence of every recognized part, instead of saving only a cleaned-up answer.&lt;/p&gt;

&lt;p&gt;The recognized work can then be compared with a solution and scoring rubric that I reviewed before the exam starts. When one important line cannot be read, the written score should stay provisional. It should not quietly become zero.&lt;/p&gt;

&lt;p&gt;I have not tested this plan yet. I need to make a small test set with real Gaokao-style handwriting, including bad handwriting and crossed-out work. Testing only the clean examples from a service demo will tell me very little.&lt;/p&gt;

&lt;h2&gt;
  
  
  I would like some advice
&lt;/h2&gt;

&lt;p&gt;Has anyone here used these tools for handwritten mathematics from phone photos?&lt;/p&gt;

&lt;p&gt;I mainly want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is Mathpix good enough for multi-line calculation?&lt;/li&gt;
&lt;li&gt;Is keeping the pen strokes worth the extra work?&lt;/li&gt;
&lt;li&gt;What open-source model should I put into the test?&lt;/li&gt;
&lt;li&gt;How do you decide that an answer is too unclear to grade?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is here: &lt;a href="https://gaokaoexam.org/" rel="noopener noreferrer"&gt;gaokaoexam.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At present I still do not know whether photo upload is practical, or whether I should ask students to write on a screen. If you have worked on a similar problem, I would like to hear what failed in your first attempt.&lt;/p&gt;

</description>
      <category>ocr</category>
      <category>machinelearning</category>
      <category>ai</category>
      <category>help</category>
    </item>
    <item>
      <title>I Stopped Tuning Prompts and Started Collecting Real Comments</title>
      <dc:creator>MRZHU</dc:creator>
      <pubDate>Tue, 12 May 2026 12:53:10 +0000</pubDate>
      <link>https://dev.to/mrzhu/i-stopped-tuning-prompts-and-started-collecting-real-comments-eg6</link>
      <guid>https://dev.to/mrzhu/i-stopped-tuning-prompts-and-started-collecting-real-comments-eg6</guid>
      <description>&lt;p&gt;When I started building an AI comment generator, my first instinct was obvious:&lt;/p&gt;

&lt;p&gt;Tune the prompt harder.&lt;/p&gt;

&lt;p&gt;I tried to make the model sound more casual. Then more specific. Then less robotic. Then more like a real TikTok user. Then less like a marketing intern pretending to be a real TikTok user.&lt;/p&gt;

&lt;p&gt;That approach worked a little.&lt;/p&gt;

&lt;p&gt;But after a while, it felt wrong.&lt;/p&gt;

&lt;p&gt;I was asking AI to invent the shape of a good comment from second-hand assumptions. I was not looking closely enough at the thing I was trying to generate.&lt;/p&gt;

&lt;p&gt;The better question was not:&lt;/p&gt;

&lt;p&gt;“How do I prompt the model to write better comments?”&lt;/p&gt;

&lt;p&gt;The better question was:&lt;/p&gt;

&lt;p&gt;“What do real high-engagement comments actually look like?”&lt;/p&gt;

&lt;p&gt;That changed the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt tuning is second-hand data
&lt;/h2&gt;

&lt;p&gt;Prompt tuning is useful, but it can become a trap.&lt;/p&gt;

&lt;p&gt;If I write:&lt;/p&gt;

&lt;p&gt;“Make this comment sound natural, casual, and engaging,”&lt;/p&gt;

&lt;p&gt;the model has to guess what “natural” means.&lt;/p&gt;

&lt;p&gt;If I write:&lt;/p&gt;

&lt;p&gt;“Make this comment sound like TikTok,”&lt;/p&gt;

&lt;p&gt;the model usually reaches for a vague internet voice. Short sentences. A little humor. Maybe an emoji. Maybe a phrase like “this is so real.”&lt;/p&gt;

&lt;p&gt;That can produce something passable, but it is still built on a stereotype of the platform.&lt;/p&gt;

&lt;p&gt;It is second-hand data.&lt;/p&gt;

&lt;p&gt;The model is not learning from what people actually liked, replied to, argued with, or remembered. It is learning from my opinion about what I think people like.&lt;/p&gt;

&lt;p&gt;That felt too weak for a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I started collecting real comments
&lt;/h2&gt;

&lt;p&gt;For this project, I started collecting high-engagement short-form video comments as research material.&lt;/p&gt;

&lt;p&gt;The goal is to keep expanding this into a much larger dataset, eventually tens of thousands of high-like and high-reply comments across different niches.&lt;/p&gt;

&lt;p&gt;That matters because “good comment” is not one thing.&lt;/p&gt;

&lt;p&gt;A good comment on a fitness transformation video is different from a good comment on a gaming clip. A good comment under a storytime video is different from a good comment under a creator advice video.&lt;/p&gt;

&lt;p&gt;If I only tune prompts, I miss that.&lt;/p&gt;

&lt;p&gt;If I look at real comments, patterns start to show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  One high-liked comment says more than a generic prompt
&lt;/h2&gt;

&lt;p&gt;One comment in my dataset had 243,581 likes:&lt;/p&gt;

&lt;p&gt;“Everyone talking about Meryl, however, no one pointed out Anne's shaky voice after Meryl's glare, that's top notch acting right there.”&lt;/p&gt;

&lt;p&gt;That comment works because it is not just praise.&lt;/p&gt;

&lt;p&gt;It does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It notices a specific detail&lt;/li&gt;
&lt;li&gt;It points out that other viewers missed it&lt;/li&gt;
&lt;li&gt;It makes a judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much stronger than:&lt;/p&gt;

&lt;p&gt;“Great acting, such an amazing scene.”&lt;/p&gt;

&lt;p&gt;The second comment is positive, but empty. It could be posted under almost any clip.&lt;/p&gt;

&lt;p&gt;The first comment proves the viewer actually saw something.&lt;/p&gt;

&lt;p&gt;That is the kind of pattern I want the product to learn from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful signal is in the pattern, not the exact sentence
&lt;/h2&gt;

&lt;p&gt;I do not want to copy high-liked comments.&lt;/p&gt;

&lt;p&gt;That would be useless and wrong.&lt;/p&gt;

&lt;p&gt;The useful part is the structure.&lt;/p&gt;

&lt;p&gt;For example, this pattern appears again and again:&lt;/p&gt;

&lt;p&gt;“Everyone is talking about X, but no one is mentioning Y.”&lt;/p&gt;

&lt;p&gt;That is a powerful comment shape because it creates a small discovery moment. The commenter is not just reacting. They are reframing what everyone else is watching.&lt;/p&gt;

&lt;p&gt;Another common pattern is:&lt;/p&gt;

&lt;p&gt;“I thought X was the point, but Y is what got me.”&lt;/p&gt;

&lt;p&gt;That works because it has a turn.&lt;/p&gt;

&lt;p&gt;Another one:&lt;/p&gt;

&lt;p&gt;“As someone who has been in this situation, this part is painfully accurate.”&lt;/p&gt;

&lt;p&gt;That works because it adds lived context.&lt;/p&gt;

&lt;p&gt;These are not magic phrases&lt;/p&gt;

&lt;p&gt;They are interaction patterns.&lt;/p&gt;

&lt;p&gt;That is what I missed when I was only tuning prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product lesson
&lt;/h2&gt;

&lt;p&gt;When I started working on Comment Generator Pro[&lt;a href="http://www.commentgenerator.pro" rel="noopener noreferrer"&gt;www.commentgenerator.pro&lt;/a&gt;], I thought the hard part would be generating enough comment variations.&lt;/p&gt;

&lt;p&gt;Now I think the hard part is building the right input system.&lt;/p&gt;

&lt;p&gt;A comment generator should not just produce text. It should protect the user from producing generic text.&lt;/p&gt;

&lt;p&gt;That means the tool needs to be grounded in real comment behavior.&lt;/p&gt;

&lt;p&gt;Not just prompt tricks.&lt;br&gt;
Not just tone sliders.&lt;br&gt;
Not just “make it casual.”&lt;/p&gt;

&lt;p&gt;Real data changes what you notice.&lt;/p&gt;

&lt;p&gt;High-like comments are often specific, but not polished.&lt;br&gt;
They are emotional, but not always positive.&lt;br&gt;
They are short, but not empty.&lt;br&gt;
They often contain a twist, a missed detail, or a personal admission.&lt;/p&gt;

&lt;p&gt;That is hard to invent from a prompt alone.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
