<?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: Ijaz Ur Rahim</title>
    <description>The latest articles on DEV Community by Ijaz Ur Rahim (@mrdebugger).</description>
    <link>https://dev.to/mrdebugger</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%2F1774984%2Ff4529691-7fc0-4487-b011-27f913a71486.png</url>
      <title>DEV Community: Ijaz Ur Rahim</title>
      <link>https://dev.to/mrdebugger</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrdebugger"/>
    <language>en</language>
    <item>
      <title>I built a daily regex game. Scoring regex fairly was the hard part</title>
      <dc:creator>Ijaz Ur Rahim</dc:creator>
      <pubDate>Sun, 04 Oct 2026 12:56:39 +0000</pubDate>
      <link>https://dev.to/mrdebugger/i-built-a-daily-regex-game-scoring-regex-fairly-was-the-hard-part-52h9</link>
      <guid>https://dev.to/mrdebugger/i-built-a-daily-regex-game-scoring-regex-fairly-was-the-hard-part-52h9</guid>
      <description>&lt;p&gt;I like regex golf: the challenge of writing the shortest pattern that does a job. So I built a daily game around it, &lt;a href="https://regexhunter.com" rel="noopener noreferrer"&gt;Regex Hunter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every day there's one hunt, the same for everyone. You get a few words to &lt;strong&gt;catch&lt;/strong&gt; and a few to &lt;strong&gt;spare&lt;/strong&gt;, and you write one pattern that matches every target and none of the decoys. Then you try to make it shorter, against a par.&lt;/p&gt;

&lt;p&gt;Here's an example board:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CATCH   nono  xyx  hahaha  abab  lolol
SPARE   ababb  abba  ab  aaa  abcab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;^(.)(.)\1&lt;/code&gt; catches all five targets, but it also hits &lt;code&gt;ababb&lt;/code&gt; and &lt;code&gt;aaa&lt;/code&gt;. Close, not a solve. That back-and-forth, where the board lights up as you type and shows you exactly what your pattern hits, is most of the fun.&lt;/p&gt;

&lt;p&gt;This post is about the less visible part: what it takes to &lt;strong&gt;score&lt;/strong&gt; regex fairly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Length is the score, so length has to be honest
&lt;/h2&gt;

&lt;p&gt;The score is your pattern's length. That sounds trivial until you decide what "length" means.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Characters, not bytes or UTF-16 units.&lt;/strong&gt; In JavaScript, &lt;code&gt;'😀'.length&lt;/code&gt; is 2. A pattern with an emoji shouldn't cost double, so the client counts code points with &lt;code&gt;[...pattern].length&lt;/code&gt;, and the server uses Python's &lt;code&gt;len()&lt;/code&gt;, which already counts code points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags cost a character.&lt;/strong&gt; Code golf has always charged for &lt;code&gt;/i&lt;/code&gt;. Without that, a case-insensitive hunt is free to trivialise. Each of &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt; that you add beyond the hunt's own flags costs one character. &lt;code&gt;g&lt;/code&gt; costs nothing because it changes nothing: a "does it match?" test ignores it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flags that change what a pattern matches; 'g' is a no-op for .test().&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCORED_FLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ims&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoredLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&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;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;flags&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SCORED_FLAGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;size&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;pattern&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;extra&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 server has the same function in Python, and a test keeps the two in step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser judges live, the server judges for real
&lt;/h2&gt;

&lt;p&gt;The board needs instant feedback, so the browser runs your pattern with JavaScript's &lt;code&gt;RegExp&lt;/code&gt; on every keystroke. A leaderboard can't trust the browser, though, so every submitted solve is checked again on the server.&lt;/p&gt;

&lt;p&gt;That server check has two jobs the browser doesn't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cap the pattern.&lt;/strong&gt; 64 characters at most. Nobody golfing needs more, and it bounds the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survive catastrophic backtracking.&lt;/strong&gt; Someone &lt;em&gt;will&lt;/em&gt; submit &lt;code&gt;(a+)+$&lt;/code&gt; against a long string. The server uses Python's &lt;a href="https://pypi.org/project/regex/" rel="noopener noreferrer"&gt;&lt;code&gt;regex&lt;/code&gt;&lt;/a&gt; module, which supports a per-match timeout, and gives every word 25 ms:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MATCH_TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.025&lt;/span&gt;  &lt;span class="c1"&gt;# seconds, catastrophic-backtracking guard
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;monsters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MATCH_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pattern does not catch all monsters.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;innocents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MATCH_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pattern wrongly catches an innocent.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout counts as a failed solve, not a server error, so a hostile pattern costs one request and nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I found while writing this post
&lt;/h2&gt;

&lt;p&gt;While pulling the snippets above, I reread the browser's solve check:&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;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flags&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;caught&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hunt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;monsters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spot it? If &lt;code&gt;flags&lt;/code&gt; contains &lt;code&gt;g&lt;/code&gt; (or &lt;code&gt;y&lt;/code&gt;), &lt;code&gt;RegExp.prototype.test&lt;/code&gt; is &lt;em&gt;stateful&lt;/em&gt;. After a match it sets &lt;code&gt;regex.lastIndex&lt;/code&gt; to where the match ended, and the next call starts searching from there, &lt;strong&gt;even on a different string&lt;/strong&gt;. So after &lt;code&gt;ca&lt;/code&gt; matches &lt;code&gt;cat&lt;/code&gt; (ending at index 2), the check on &lt;code&gt;car&lt;/code&gt; starts at index 2, finds nothing, and the board says your perfectly good pattern misses a target.&lt;/p&gt;

&lt;p&gt;No live hunt uses &lt;code&gt;g&lt;/code&gt; today, so no one was hit by it, but the hunt editor allows it. The fix is one line: start every word from zero.&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;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastIndex&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;return&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&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;I added a test that fails without it. It's a good reminder that a &lt;code&gt;g&lt;/code&gt; regex is an iterator wearing a boolean's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Par: what counts as "good"?
&lt;/h2&gt;

&lt;p&gt;A score needs a reference point. Each hunt has a &lt;strong&gt;par&lt;/strong&gt;: the length of a good, not heroic, solution. Points are &lt;code&gt;100 × par ÷ your length&lt;/code&gt;, capped at 150, so matching par earns 100 and beating it earns more, up to a ceiling. The cap matters because otherwise one absurd 3-character trick on an easy hunt outweighs a week of solid play.&lt;/p&gt;

&lt;p&gt;Community hunts add a wrinkle: anyone can write one and set its par. To stop a generous par from inflating scores, a community hunt's par is capped at 1.5× the shortest solve anyone has found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fairness beyond the pattern
&lt;/h2&gt;

&lt;p&gt;A few smaller decisions turned out to matter as much as the regex engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archive runs never touch your streak.&lt;/strong&gt; Replaying last Tuesday's hunt is practice, not a do-over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rankings by concept.&lt;/strong&gt; One overall number hides a lot, so there are separate boards for anchors, character classes, quantifiers, backreferences and lookarounds. You can be top 5% at classes and still learning lookarounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer has three modes.&lt;/strong&gt; In &lt;em&gt;Shortest&lt;/em&gt;, fewest characters wins. In &lt;em&gt;Fastest&lt;/em&gt;, the quickest valid solve wins. In &lt;em&gt;Reverse&lt;/em&gt;, you see only the board and no description, and golf blind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Everything else in it
&lt;/h2&gt;

&lt;p&gt;Scoring is the core, but the daily hunt is only one way in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily hunt&lt;/strong&gt;: one shared hunt a day, with streaks, stars, a leaderboard and a share card for your result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive&lt;/strong&gt;: replay any past daily on its own leaderboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice packs&lt;/strong&gt;: 16 short ladders, each drilling one technique, from shorthand classes and anchors to backreferences, validation and multiline work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Academy&lt;/strong&gt;: nine interactive tracks from your first literal to golf-grade patterns. Finishing a track earns a certificate anyone can verify on the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide&lt;/strong&gt;: a complete regex reference where every example is live and editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playground&lt;/strong&gt;: test any pattern against your own text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community hunts&lt;/strong&gt;: write your own hunt in the Studio, share it with a short link, and discuss solutions in threaded comments with upvotes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hunting Spree&lt;/strong&gt;: real-time multiplayer races in Shortest, Fastest or Reverse mode, public or private.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams&lt;/strong&gt;: a shared hunt library and members-only sprees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rankings&lt;/strong&gt;: an overall board plus one per regex concept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avatars&lt;/strong&gt;: build your own from parts. A few parts are earned, not picked: finish an Academy track, win a spree, or keep a 30-day streak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two languages&lt;/strong&gt;: English and Chinese.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The daily is free and needs no account: &lt;strong&gt;&lt;a href="https://regexhunter.com" rel="noopener noreferrer"&gt;regexhunter.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you've found a shorter pattern than you think anyone could, I'd love to see it in the comments, along with what the scoring should do about a regex trick you think is unfair.&lt;/p&gt;

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