<?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: Gowtham R</title>
    <description>The latest articles on DEV Community by Gowtham R (@gowtham_r_2002).</description>
    <link>https://dev.to/gowtham_r_2002</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%2F3974315%2F15db50d0-1d24-498b-89e8-fd464e23847d.png</url>
      <title>DEV Community: Gowtham R</title>
      <link>https://dev.to/gowtham_r_2002</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gowtham_r_2002"/>
    <language>en</language>
    <item>
      <title>One Seed, Same Puzzle for Everyone: Seeded RNG for a Fair Daily Challenge</title>
      <dc:creator>Gowtham R</dc:creator>
      <pubDate>Tue, 09 Jun 2026 14:04:21 +0000</pubDate>
      <link>https://dev.to/gowtham_r_2002/one-seed-same-puzzle-for-everyone-seeded-rng-for-a-fair-daily-challenge-1h7d</link>
      <guid>https://dev.to/gowtham_r_2002/one-seed-same-puzzle-for-everyone-seeded-rng-for-a-fair-daily-challenge-1h7d</guid>
      <description>&lt;p&gt;A few months ago I built a daily brain-training game with prize tournaments — ten small cognitive games, a fresh challenge every day, and a leaderboard that decides who wins real rewards. The first design question wasn't about graphics or game feel. It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If everyone is competing on a "random" puzzle, how do you make sure everyone got the &lt;em&gt;same&lt;/em&gt; random puzzle — and that nobody can lie about their score?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are actually two problems, and they pull in opposite directions. Fairness wants the randomness to be &lt;em&gt;shared and predictable&lt;/em&gt; — every player on a given day should face the identical challenge, or the leaderboard is meaningless. Anti-cheat wants the server to be able to &lt;em&gt;independently verify&lt;/em&gt; a score without trusting the player's device. The thing that solves both at once is a seeded RNG.&lt;/p&gt;

&lt;p&gt;This post is about that pattern: how a single short string makes a "random" puzzle identical for every player and recomputable on the server, why I deliberately made the seed &lt;em&gt;public&lt;/em&gt; (the opposite of what you'd do for a high-score game), and the one game that broke the whole model and forced me to think differently.&lt;/p&gt;

&lt;p&gt;A quick note on stack: my client is Flutter (Dart) and my backend is TypeScript edge functions, so that's what the snippets look like. But the idea is language-agnostic — it works the same with Kotlin, React Native, or anything else. The trick isn't the language, it's getting two different runtimes to agree on what "random" means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: randomness from a string
&lt;/h2&gt;

&lt;p&gt;A normal random number generator gives you different numbers every time, which is exactly what you &lt;em&gt;don't&lt;/em&gt; want here. If my phone rolls a different set of math problems than yours, we're not playing the same game, so comparing our scores is nonsense.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;seeded&lt;/em&gt; RNG fixes this. You give it a starting value — the seed — and from that point it produces a fixed, repeatable sequence of "random" numbers. Same seed in, same sequence out, every single time, on every device. It only looks random; it's completely deterministic.&lt;/p&gt;

&lt;p&gt;So the daily challenge seed is just a human-readable string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-03-16-speed_calc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Date, plus the game name. That's it. Everyone playing Speed Calc on March 16th derives their puzzle from that exact string, so everyone gets the exact same equations in the exact same order. Tomorrow the date changes, the seed changes, and a fresh challenge appears for everyone at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;dailySeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GameType&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dateStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'yyyy-MM-dd'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$dateStr&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;${game.seedName}&lt;/span&gt;&lt;span class="s"&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;There's no server round-trip to "fetch today's puzzle." The client can generate the whole challenge offline from the date, and the server can regenerate the identical challenge whenever it needs to check a score. The seed is the single source of truth, and it weighs almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RNG itself
&lt;/h2&gt;

&lt;p&gt;The generator is small on purpose. It takes the seed string, hashes it with SHA-256 to get a stable starting state, then runs a classic 48-bit linear congruential generator — the same constants you'd recognize from older Java &lt;code&gt;Random&lt;/code&gt; implementations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SeededRandom&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x5DEECE66D&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_increment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;SeededRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_deriveSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_deriveSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;byteData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ByteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;byteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt64&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;nextDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;_multiplier&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;_increment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_mask&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nextInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nextInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min&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;Nothing here is cryptographically fancy, and it doesn't need to be — the goal isn't unpredictability, it's &lt;em&gt;reproducibility&lt;/em&gt;. The most important comment in my entire codebase lives right above this class: &lt;em&gt;"same seed always produces the same sequence across Dart and TypeScript."&lt;/em&gt; That cross-runtime guarantee is the whole foundation. If the Dart client and the TypeScript server ever disagree about what number comes next, everything downstream falls apart.&lt;/p&gt;

&lt;p&gt;On top of the raw generator there's a seeded Fisher-Yates shuffle, so a game can pre-generate a pool of, say, 30 equations and shuffle them deterministically. Both client and server build the pool, shuffle it with the same RNG calls in the same order, and walk through it with a cursor. As long as both sides consume the RNG identically, they stay in lockstep forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made the seed &lt;em&gt;public&lt;/em&gt; (and when you shouldn't)
&lt;/h2&gt;

&lt;p&gt;Here's the design decision I want to call out, because it's the opposite of what a lot of anti-cheat advice tells you to do.&lt;/p&gt;

&lt;p&gt;For a high-score game where each run should be unguessable, you'd want a &lt;em&gt;secret, per-run&lt;/em&gt; seed issued by the server right before play — so the player can't precompute or predict the run. Unpredictability is the security property there.&lt;/p&gt;

&lt;p&gt;A daily challenge is the inverse. The seed is &lt;em&gt;derived from the public date&lt;/em&gt;, so it's completely predictable — anyone can work out tomorrow's seed. And that's fine, because fairness, not secrecy, is the goal. Everyone facing the identical puzzle is a &lt;em&gt;feature&lt;/em&gt;. Knowing the seed in advance doesn't help you cheat, because the seed only tells you &lt;em&gt;which&lt;/em&gt; equations you'll get — it doesn't solve them for you, and it doesn't let you submit a fake score.&lt;/p&gt;

&lt;p&gt;That last part is the key, and it's where validation comes in. A public seed is only safe if the &lt;strong&gt;server independently recomputes the result&lt;/strong&gt; instead of trusting the client. The seed makes the puzzle fair; the server-side recompute makes the score honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation: the server recomputes, then verifies
&lt;/h2&gt;

&lt;p&gt;Every score submission goes through a &lt;code&gt;validate-score&lt;/code&gt; function before it counts. The shape is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate and rate-limit&lt;/strong&gt; the user (caps per minute and per hour, enforced in the database so they survive cold starts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the request signature.&lt;/strong&gt; Each submission is HMAC-SHA256 signed with a secret compiled into the app, plus a timestamp and a one-time nonce. The server rebuilds the signature over &lt;code&gt;nonce.timestamp.body&lt;/code&gt;, compares it in constant time, rejects anything older than a 10-second window, and burns the nonce so the same request can't be replayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recompute the puzzle from the seed&lt;/strong&gt; and check the player's answers against it.
That third step is the payoff of the whole seeded design. For Speed Calc, the server re-derives the exact same shuffled equation pool the player saw — same seed, same RNG call order, same ranges — and checks each submitted answer against the equation it &lt;em&gt;knows&lt;/em&gt; was on screen. The client's reported score is never trusted; it's recomputed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The catch is that the two sides have to generate equations &lt;em&gt;identically&lt;/em&gt;, in two different languages. Here's the same level-2 equation (addition or subtraction, operands 1–50, subtraction arranged to stay non-negative) in the Dart client and the TypeScript server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dart — client&lt;/span&gt;
&lt;span class="n"&gt;Equation&lt;/span&gt; &lt;span class="nf"&gt;_level2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isAdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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="n"&gt;isAdd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Equation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;expression:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$a&lt;/span&gt;&lt;span class="s"&gt; + &lt;/span&gt;&lt;span class="si"&gt;$b&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;answer:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;difficulty:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&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="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// keep result non-negative&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Equation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;expression:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$a&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;$b&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;answer:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;difficulty:&lt;/span&gt; &lt;span class="mi"&gt;2&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript — server. MUST match Dart exactly: same RNG call order, same ranges.&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&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;isAdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&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;isAdd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expression&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;a&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;b&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="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expression&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;a&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;b&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="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&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;Look closely at the order of the RNG calls — &lt;code&gt;nextInt(2)&lt;/code&gt; for the operator, then &lt;code&gt;range(1, 51)&lt;/code&gt; twice — because that order &lt;em&gt;is&lt;/em&gt; the contract. The server has to pull values off the generator in precisely the same sequence as the client, or the two pools diverge and a perfectly honest player's score fails to validate. The comment in the server file isn't decoration; it's a warning to my future self.&lt;/p&gt;

&lt;p&gt;For nine of the ten games, this is clean: the puzzle is pure logic, there's a single correct answer per question, and the server can reproduce it exactly. And then there was the tenth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The game that broke the model: Reaction Tap
&lt;/h2&gt;

&lt;p&gt;Every other game is &lt;em&gt;computed&lt;/em&gt;. An equation has one right answer. A scrambled word unscrambles to one word. A pattern has one next step. The server regenerates the puzzle and checks your answer against a known-correct value. Simple.&lt;/p&gt;

&lt;p&gt;Reaction Tap isn't like that, and it was genuinely the hard one to figure out. The game shows a target after a random delay and measures how fast you tap it. The &lt;em&gt;delay&lt;/em&gt; is seeded — the server can regenerate exactly when the target appeared. But the &lt;strong&gt;score depends on your reaction time, which is a human measurement the server never witnessed.&lt;/strong&gt; There's no "correct answer" to recompute. I couldn't validate it the way I validated the other nine, and that's the part that took me a while to get right.&lt;/p&gt;

&lt;p&gt;The realization was that you stop trying to recompute the score and instead validate the &lt;em&gt;boundaries of what's physically possible&lt;/em&gt;. The server still advances the RNG through every round to stay in sync (each round consumes three values — the delay and the target's x/y position), but the actual checks are about plausibility:&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;MIN_REACTION_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&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;tooFast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reactionMs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_REACTION_MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A sub-100ms reaction can never legitimately be a correct hit&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;tooFast&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;correct&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reaction below human minimum — cannot be correct&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;span class="c1"&gt;// Points are recomputed server-side, never trusted from the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tooFast&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;reactionMs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Negative reaction time is impossible, so it's rejected. A reaction under ~100ms is faster than a human nervous system can actually respond, so it can't count as a legitimate hit — a tap-ahead bot or a patched timer would show up here. And the points themselves are recomputed from the reported time with the same formula the client uses, so a client that fibs about its score gets overridden.&lt;/p&gt;

&lt;p&gt;It's a different &lt;em&gt;shape&lt;/em&gt; of validation — verifying a plausible range instead of recomputing an exact answer — and it's the case that taught me the seeded-recompute pattern has a hard edge: it works beautifully for anything deterministic, and not at all for anything that depends on real-world human timing. For those, you fall back to trust boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take to the next project
&lt;/h2&gt;

&lt;p&gt;If you're building anything where players compete on shared randomness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Derive shared puzzles from a seed&lt;/strong&gt;, not from a per-device random call, so everyone genuinely plays the same thing and the server can reproduce it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick your seed's secrecy to match your goal.&lt;/strong&gt; Public, predictable seeds for &lt;em&gt;fairness&lt;/em&gt; (daily challenges). Secret, server-issued seeds for &lt;em&gt;unpredictability&lt;/em&gt; (high-score runs). Same tool, opposite settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A seed is only safe if the server recomputes the outcome.&lt;/strong&gt; Sharing the puzzle is fine; trusting the score is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch out for anything that isn't purely computed.&lt;/strong&gt; Reaction time, sensor input, anything measured rather than derived — you can't recompute it, so validate the physically-possible range instead.
The seeded RNG ended up being maybe sixty lines of code. But it quietly answers both of the questions I started with — is this fair, and is this real — and that's most of what a competitive game actually needs.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>rng</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Don't Trust the Score: Building Server-Authoritative Validation for a Prize-Based Mobile Game</title>
      <dc:creator>Gowtham R</dc:creator>
      <pubDate>Mon, 08 Jun 2026 14:08:00 +0000</pubDate>
      <link>https://dev.to/gowtham_r_2002/dont-trust-the-score-building-server-authoritative-validation-for-a-prize-based-mobile-game-246i</link>
      <guid>https://dev.to/gowtham_r_2002/dont-trust-the-score-building-server-authoritative-validation-for-a-prize-based-mobile-game-246i</guid>
      <description>&lt;p&gt;When there's no money involved, a cheater in your game is mostly an annoyance. A leaderboard gets polluted, someone brags about a fake high score, you sigh and move on.&lt;/p&gt;

&lt;p&gt;The moment a score can be converted into a real prize — a gift card, a draw entry, actual money — that calculation changes completely. Now a "high score" is a withdrawal request. And the uncomfortable truth I had to start from while building a mobile arcade game where gameplay earns entries into prize draws is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any client-side game can be modified by a motivated attacker.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's not pessimism, it's just the reality of shipping a game on someone else's device. The APK can be patched to submit fake scores. Methods can be hooked at runtime on a rooted device. Old winning requests can be replayed. Local timers and collision checks can be edited. And the backend endpoint that records scores can be called directly with a forged payload, skipping the game entirely.&lt;/p&gt;

&lt;p&gt;So the design principle the whole system is built on is blunt: &lt;strong&gt;the game client is an input device, not an authority.&lt;/strong&gt; It is allowed to tell the server what the player &lt;em&gt;did&lt;/em&gt;. It is never allowed to tell the server what the player &lt;em&gt;earned&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A quick note on stack before we go further: my client is built in Flutter and my backend runs as TypeScript edge functions, so that's what the code samples look like. But nothing in this post is Flutter-specific. The model — server-issued seeds, deterministic gameplay, server-side replay, one-time run IDs — applies exactly the same whether your client is Flutter, Kotlin, React Native, or Unity. The language changes; the principle doesn't. Treat the snippets as illustration, not prescription.&lt;/p&gt;

&lt;p&gt;This post walks through how I built that — the seed-and-replay model that makes it work, the specific checks that close off each attack, and the things that turned out to be harder than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version (and why it dies instantly)
&lt;/h2&gt;

&lt;p&gt;The obvious first design is the one almost every tutorial shows you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Player finishes game → app sends { score: 4820 } → server saves 4820
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for a casual leaderboard. For a prize game it's indefensible. There is nothing stopping anyone from opening up a network inspector, watching that request, and replaying it with &lt;code&gt;score: 999999&lt;/code&gt;. You haven't built a game backend, you've built a "type your own prize" form.&lt;/p&gt;

&lt;p&gt;The first instinct people have is to "sign" the score on the client, or obfuscate the app, or add a Play Integrity check and call it done. None of that fixes the core problem, because the secret used to sign lives &lt;em&gt;on the device the attacker controls&lt;/em&gt;, and integrity checks tell you the binary looks legit — they can't tell you whether the player actually survived 45 seconds or cleared six lines. You can't patch your way out of trusting an untrusted machine.&lt;/p&gt;

&lt;p&gt;The only thing that actually works is to stop trusting the score and make the server compute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: deterministic seeds + replay
&lt;/h2&gt;

&lt;p&gt;Here's the model I used.&lt;/p&gt;

&lt;p&gt;Every game in the app is &lt;strong&gt;deterministic&lt;/strong&gt;. Given the same starting seed, the game world unfolds identically every single time — the same Snake food spawns in the same cells, the same Tetris-style pieces come out of the bag in the same order. There's no "true" randomness in the gameplay loop; everything random is driven by a pseudo-random generator that's seeded from a single value.&lt;/p&gt;

&lt;p&gt;Because of that property, the server and the client can run the &lt;em&gt;exact same game&lt;/em&gt; from the &lt;em&gt;exact same seed&lt;/em&gt; and get the &lt;em&gt;exact same world&lt;/em&gt;. That's the whole trick. If the server can reproduce your game, the server doesn't need to believe your score — it can recompute it.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Player starts a mission.&lt;/strong&gt; The client calls &lt;code&gt;start-mission-run&lt;/code&gt;. The server creates a one-time run, generates a fresh random seed, stamps a rules version and a 15-minute expiry, and returns the seed to the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Player plays.&lt;/strong&gt; The Flutter game seeds its RNG with the server's seed and runs normally. While playing, it records a compact &lt;strong&gt;transcript&lt;/strong&gt; of the player's inputs — every direction change, every move, rotate, hold, and hard-drop, each tagged with the game tick it happened on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Player submits.&lt;/strong&gt; Instead of sending a score, the client sends the transcript, a hash of that transcript, and the run ID it was issued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server replays.&lt;/strong&gt; &lt;code&gt;submit-score&lt;/code&gt; loads the run, takes the original seed, re-runs the player's recorded inputs through a server-side copy of the game logic, and computes the authoritative score and completion itself.
The score the client &lt;em&gt;claims&lt;/em&gt; is, at best, used for analytics. It is never what awards a prize.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The deterministic RNG
&lt;/h2&gt;

&lt;p&gt;The piece that makes all of this possible is small and unglamorous — a seeded pseudo-random generator that behaves identically on Dart (client) and on the Deno/TypeScript backend.&lt;/p&gt;

&lt;p&gt;The seed comes in as a string, so first it gets folded into a 32-bit integer (an FNV-1a-style hash), then each "random" value is produced by a classic linear congruential generator:&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;function&lt;/span&gt; &lt;span class="nf"&gt;seedInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&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;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2166136261&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;seed&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;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;hash&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;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16777619&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;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&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;nextRand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1664525&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1013904223&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic here — and that's the point. It's deterministic, it's portable, and it produces the same stream of numbers on both sides as long as both sides advance it the same number of times. When Snake needs to spawn food, both the device and the server call &lt;code&gt;nextRand&lt;/code&gt; and map the result onto the grid the same way:&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;function&lt;/span&gt; &lt;span class="nf"&gt;nextFood&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nextRand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;cols&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="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;occupied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&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;candidate&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;Same seed, same sequence, same food. The server can sit down and play your exact game from your exact inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What submit-score actually checks
&lt;/h2&gt;

&lt;p&gt;Replay is the heart of it, but on its own it isn't enough. The submission endpoint is layered, and each layer closes off a specific attack. Here's what a score submission goes through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits, including one-time IDs.&lt;/strong&gt; There's a burst limit and a daily limit per user, but the important two are the single-use ones: the &lt;code&gt;clientRunId&lt;/code&gt; and the server-issued run ID are each capped at exactly one submission per day. That's what makes a replay attack pointless — resending a captured winning request just gets rejected as a duplicate.&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;await&lt;/span&gt; &lt;span class="nf"&gt;enforceRateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit-score:server-run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serverRunId&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="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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;&lt;strong&gt;Run state and expiry.&lt;/strong&gt; The run has to actually exist, belong to this user, match this mission and game, still be in the &lt;code&gt;issued&lt;/code&gt; state (not already submitted — otherwise it's a &lt;code&gt;409 already submitted&lt;/code&gt;), and not be past its 15-minute expiry. A stale or reused run never reaches the scoring logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transcript hash.&lt;/strong&gt; The server recomputes a SHA-256 over the submitted transcript and compares it to the hash the client sent. A mismatch doesn't necessarily mean malice, but it gets recorded as a risk flag — and risk flags matter, because:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replay decides everything.&lt;/strong&gt; The server feeds the seed and the player's inputs into its own copy of the game and produces the authoritative score, duration, and completion. A mission only counts as completed when the replay succeeds &lt;em&gt;and there are zero risk flags&lt;/em&gt;. Anything flagged — hash mismatch, rules version mismatch, a replay that doesn't add up — doesn't get auto-rewarded; it gets held for review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play Integrity, bound to the payload.&lt;/strong&gt; On Android, the app fetches a Play Integrity token right before submitting and binds it to a hash of the exact score payload. The server decodes that token using Google's API (server-side — on-device checks are bypassable), confirms the binary is a recognized Play build on a device that meets integrity, and stores the verdict. Crucially, this is treated as &lt;em&gt;one risk signal among many&lt;/em&gt;, not as proof of a legitimate score. Integrity tells you the app and device look real. It cannot tell you the player earned the points.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that was harder than I expected: Block Drop
&lt;/h2&gt;

&lt;p&gt;Most of the games were straightforward to make replay-safe. Block Drop, my Tetris-style game, was not. It has the biggest state space of anything in the app — a full board, a bag of upcoming pieces, plus moves, rotations, holds, and hard-drops — and all of it has to reproduce identically on the server. So it became the game that ate most of my time, and it broke in two separate phases.&lt;/p&gt;

&lt;p&gt;The first round of problems was the gameplay itself. Before I could even think about validation, Block Drop had UI glitches I had to chase down — pieces and board state not behaving the way they should on the device. That's the unglamorous reality of this kind of game: you can't validate a game that isn't yet behaving correctly, because you don't have a stable "correct" to compare against. So step one was just getting the game to play right.&lt;/p&gt;

&lt;p&gt;Then I fixed the UI, and a &lt;em&gt;second&lt;/em&gt; class of problems showed up — the server-side validation ones. With the game finally behaving on the phone, the server replay still didn't line up with what had happened on the device. And this is the trap with a game this complex: it's not enough for the game to look right on screen. The server has to land on the &lt;em&gt;exact same board state&lt;/em&gt; the player saw, from the same seed, after the same sequence of inputs. Any tiny divergence between how the Dart client and the TypeScript server handle a piece compounds, and by the end of a run the two sides disagree about the score entirely.&lt;/p&gt;

&lt;p&gt;The fix that mattered was a design decision: don't trust the client's summary of what happened, replay the board itself. An earlier instinct is to let the client report higher-level events — "I cleared these lines" — and have the server tally those up. But the server can't actually verify a reported line-clear; it just has to take the client's word for it, which defeats the whole point. So Block Drop's replay is &lt;strong&gt;board-state based&lt;/strong&gt;. The server regenerates the deterministic piece bag from the seed, applies the player's recorded controls one tick at a time, and works out the line clears &lt;em&gt;itself&lt;/em&gt; from the resulting board:&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;function&lt;/span&gt; &lt;span class="nf"&gt;lockPiece&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// place the current piece into the board grid&lt;/span&gt;
  &lt;span class="c1"&gt;// then clear completed lines based on the actual board,&lt;/span&gt;
  &lt;span class="c1"&gt;// not on anything the client reported&lt;/span&gt;
  &lt;span class="nf"&gt;clearLines&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;spawn&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;If you take one thing from this section: the hardest part of server-authoritative validation isn't the security idea, it's the determinism. Making the &lt;em&gt;same&lt;/em&gt; logic produce &lt;em&gt;identical&lt;/em&gt; results in two languages, across two runtimes, every time — that's the real work. Block Drop is also the game I'd tell anyone to test most aggressively, precisely because its state space is large enough to hide a desync you won't notice until a real player's score quietly fails to validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building a game that pays out
&lt;/h2&gt;

&lt;p&gt;If you're putting real value behind a score, here's the short version of everything above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never let the client be the source of truth for anything that earns money.&lt;/strong&gt; It reports inputs. The server decides outcomes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your gameplay deterministic from a server-issued seed&lt;/strong&gt;, so the backend can independently reproduce and score the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue one-time run IDs and reject duplicates.&lt;/strong&gt; This is what neutralizes replay attacks, and it's cheap to build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat Play Integrity / attestation as a signal, not a verdict.&lt;/strong&gt; It catches tampered binaries and sketchy devices. It does not prove a score is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't auto-pay flagged runs.&lt;/strong&gt; Server-authoritative scoring plus conservative eligibility plus manual review on high-value events is what keeps the economy honest.
None of these individually is clever. Stacked together, they move you from "anyone can type their own prize" to "you'd have to defeat a server that already knows what should have happened." For a game where a high score is a withdrawal request, that's the bar.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>gamedev</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
