<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RarestPick]]></title><description><![CDATA[RarestPick]]></description><link>https://rarestpick.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:14:34 GMT</lastBuildDate><atom:link href="https://rarestpick.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I built a daily trivia game where the rarest answer wins (Wikidata + Cloudflare D1)]]></title><description><![CDATA[Most trivia games reward the answer everyone knows. I wanted the opposite: a game where "Nigeria" is a perfectly good answer to name a country in Africa…]]></description><link>https://rarestpick.hashnode.dev/how-i-built-a-daily-trivia-game-where-the-rarest-answer-wins-wikidata-cloudflare-d1</link><guid isPermaLink="true">https://rarestpick.hashnode.dev/how-i-built-a-daily-trivia-game-where-the-rarest-answer-wins-wikidata-cloudflare-d1</guid><category><![CDATA[GameDev]]></category><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Wikidata]]></category><dc:creator><![CDATA[RarestPick]]></dc:creator><pubDate>Tue, 27 Oct 2026 15:38:37 GMT</pubDate><content:encoded><![CDATA[<p>Most trivia games reward the answer everyone knows. I wanted the opposite: a game where "Nigeria" is a perfectly good answer to <em>name a country in Africa</em>, but "Eswatini" is a much better one. Being right is required. Being right in a way nobody else thought of is the actual game.</p>
<p>That game is <strong>RarestPick</strong>: <a href="https://rarestpick.com/">https://rarestpick.com/</a> (free, no account).</p>
<p>The format: 7 prompts a day, 20 seconds each, everyone gets the same set. You type a free-text answer, and the less common your correct answer is among all players, the more points you get. A new set goes live at midnight Eastern Time.</p>
<p>It's inspired by the rare-answer daily games that have been popping up lately, and the codebase started from the MIT-licensed openkrillion project. The question bank, scoring implementation and UI are my own, and it isn't affiliated with Krillion. This post covers the parts that turned out to be the interesting engineering problems.</p>
<h2>Problem 1: you need <em>complete</em> answer lists</h2>
<p>A multiple-choice quiz needs one right answer. A rare-answer game needs <strong>every</strong> right answer, because the whole point is that people go looking for the obscure ones. If someone types "Comoros" and the game says "not on our list", the game is broken.</p>
<p>Hand-writing hundreds of complete lists isn't realistic, so the lists are generated from <strong>Wikidata</strong> with SPARQL. A question is basically a prompt plus a query. The general shape looks like this:</p>
<pre><code class="language-sparql">SELECT ?item ?label (GROUP_CONCAT(DISTINCT ?alt; separator="|") AS ?aliases)
       ?article ?sitelinks WHERE {
  ?item wdt:P31 wd:Q6256 ;      # instance of: country
        wdt:P30 wd:Q15 .        # continent: Africa
  ?item rdfs:label ?label . FILTER(LANG(?label) = "en")
  OPTIONAL { ?item skos:altLabel ?alt . FILTER(LANG(?alt) = "en") }
  OPTIONAL { ?article schema:about ?item ;
                      schema:isPartOf &lt;https://en.wikipedia.org/&gt; . }
  ?item wikibase:sitelinks ?sitelinks .
}
GROUP BY ?item ?label ?article ?sitelinks
ORDER BY DESC(?sitelinks)
</code></pre>
<p>Swap the triple pattern and you get a different question: dog breeds, chemical elements, Pixar movies, and so on. The game currently has 8 categories: geography, movies &amp; TV, food, animals, sports, music, science and history.</p>
<p>A few lessons from doing this:</p>
<ul>
<li><strong><code>skos:altLabel</code> is gold.</strong> Wikidata aliases give you "Swaziland" for Eswatini and "Ivory Coast" for CÃ´te d'Ivoire for free.</li>
<li><strong>Wikidata is crowdsourced, so class membership is messy.</strong> <code>P31</code>/<code>P279</code> (instance of / subclass of) are sometimes missing or wrong, so a query can leak odd entries or miss obvious ones. I lean on items that have an English Wikipedia article and run automated checks on every list. The checks cover list size, ambiguous labels, aliases that collide with another answer, and duplicate questions.</li>
<li><strong>Avoid "current" anything</strong> unless the prompt is pinned to a date. Lists that change over time make for angry players.</li>
<li><strong>Players are the final QA.</strong> There's a "My answer should count" button on the result page. Missing answers get reported and can be added for future players.</li>
</ul>
<h2>Problem 2: matching messy free-text input</h2>
<p>You have 20 seconds and a phone keyboard, so people make typos. The matcher goes roughly like this:</p>
<ol>
<li><strong>Normalize</strong> both the input and every answer/alias. That means Unicode decomposition to strip accents, lowercasing, <code>&amp;</code> â†’ <code>and</code>, dropping punctuation and spaces, and dropping a leading "the / a / an".</li>
<li><strong>Exact match</strong> against the normalized canonical answers plus the alias index.</li>
<li><strong>Fuzzy fallback</strong> for small, unambiguous typos, so "Nigria" becomes Nigeria. The correction is shown back to the player, so it's transparent.</li>
</ol>
<p>The important word is <em>unambiguous</em>. Short inputs, or inputs that sit close to two different valid answers, get rejected instead of guessed. A wrong answer doesn't lock you out either. You get "Not on our list, try again" and the clock keeps running.</p>
<h2>Problem 3: rarity, and the cold-start problem</h2>
<p>Scoring is simple once you have data: count how many players picked each answer and turn the share into a tier.</p>
<table>
<thead>
<tr>
<th>Tier</th>
<th>Share of answers</th>
<th>Points</th>
</tr>
</thead>
<tbody><tr>
<td>Common</td>
<td>20%+</td>
<td>10</td>
</tr>
<tr>
<td>Familiar</td>
<td>10%+</td>
<td>25</td>
</tr>
<tr>
<td>Uncommon</td>
<td>5%+</td>
<td>45</td>
</tr>
<tr>
<td>Rare</td>
<td>2%+</td>
<td>65</td>
</tr>
<tr>
<td>Epic</td>
<td>0.5%+</td>
<td>85</td>
</tr>
<tr>
<td>Rarest Pick</td>
<td>under 0.5%</td>
<td>100</td>
</tr>
</tbody></table>
<p>The problem is day one. A brand-new question has zero plays, and a brand-new <em>game</em> has very few players. With 3 players, every answer is "rare".</p>
<p>The fix is a <strong>seed estimate</strong> from public Wikipedia signals. For each answer I use the page's views, the number of inbound links, and the number of language editions. Popular things (Nigeria, Egypt) get a big share of an imaginary reference crowd, and obscure ones get a tiny share. The real count for an answer is then roughly:</p>
<pre><code>count = seed_count * seed_weight + real_count
</code></pre>
<p><code>seed_weight</code> starts at 1 and fades as real plays accumulate, until scoring is driven purely by what real players typed. Fame on Wikipedia is a decent proxy for "what comes to mind first". It's not perfect, which is why it only covers the cold start.</p>
<p>Because shares move as more people play, scores during the day are provisional. Leaderboard scores are recalculated every ten minutes and freeze at midnight ET.</p>
<h2>Stack</h2>
<ul>
<li><strong>Cloudflare Workers</strong> for the app and API. The server owns the timer: there's a 3-second reading phase, then a 20-second answer window enforced server-side with a small network grace period.</li>
<li><strong>Cloudflare D1</strong> (SQLite) for questions, answers, the alias index and plays.</li>
<li>An offline build script runs the SPARQL queries, pulls the Wikipedia stats, computes seeds, runs the checks and imports the result into D1.</li>
<li>No accounts. A random device ID is enough to compute rarity and keep one daily run per player.</li>
</ul>
<p>There's also an <strong>Unlimited mode</strong> by category for practice (3 free rounds a day, optional one-time unlock). It never touches the daily leaderboard, and paying doesn't change scoring.</p>
<h2>What I'd do differently</h2>
<ul>
<li>Start the answer-report loop earlier. Players find gaps in Wikidata lists faster than any automated check.</li>
<li>Spend more time on prompt wording. "Name a cheese" is easy to query. Deciding whether a regional variant counts is a product decision, not a data one.</li>
</ul>
<h2>Try it</h2>
<p>If you like weird data problems or just want to beat the crowd, today's puzzle is at <strong><a href="https://rarestpick.com/">https://rarestpick.com/</a></strong>. I'd especially love reports of answers you think are missing or questions that feel ambiguous. That feedback goes straight into the answer lists.</p>
]]></content:encoded></item></channel></rss>