<?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: christianvadillo</title>
    <description>The latest articles on DEV Community by christianvadillo (@christianvadillo).</description>
    <link>https://dev.to/christianvadillo</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%2F4128338%2Fc166b16e-1425-40e2-898f-a1cfdff5d4e0.png</url>
      <title>DEV Community: christianvadillo</title>
      <link>https://dev.to/christianvadillo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christianvadillo"/>
    <language>en</language>
    <item>
      <title>Making players prove it: validating that a SQL query derives an answer</title>
      <dc:creator>christianvadillo</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:02:36 +0000</pubDate>
      <link>https://dev.to/christianvadillo/making-players-prove-it-validating-that-a-sql-query-derives-an-answer-44j2</link>
      <guid>https://dev.to/christianvadillo/making-players-prove-it-validating-that-a-sql-query-derives-an-answer-44j2</guid>
      <description>&lt;p&gt;I like detective stories and I wanted to get better at SQL, and at some point it stopped being two things. Both are the same move: you have a question, a pile of evidence that doesn't answer it directly, and you poke at it until it does.&lt;/p&gt;

&lt;p&gt;So I built a game where the clues are tables and the only tool is a SQL console. Twelve rows the first night, a door reader, a missing file. You write queries until you know who did it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqvme557s89zpatc0sh1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqvme557s89zpatc0sh1.jpg" alt="The case file on the left, the SQL console in the middle, the accusation form on the right" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the shape of it. Case 2 gives you a witness who needs new glasses and five details: the man was at least 1.80 m, light eyes, an anchor tattoo on the forearm, worked the crowbar left-handed, no glasses. Then it hands you the municipal census, fifty-one rows, and asks which resident meets all five at once.&lt;/p&gt;

&lt;p&gt;The rule that turned it from a quiz into a game came later, and it's the part worth writing about: &lt;strong&gt;naming the culprit isn't enough. You hand in the query that proves it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The version that didn't work
&lt;/h2&gt;

&lt;p&gt;The first implementation was the obvious one. You type the name, the engine compares it against the expected answer, done. It worked, and it was boring, because the SQL was optional. You could stare at twelve rows, spot the name with your eyes, type it, and the game would congratulate you on your query skills.&lt;/p&gt;

&lt;p&gt;So I added the proof: alongside the accusation you submit the query that supports it. The game runs it and checks that the answer appears in the result.&lt;/p&gt;

&lt;p&gt;That lasted until I tried to break it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'Ada Ferrán'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'22:20'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accepted. The one gesture of inference the game demanded was, in practice, optional — and now it even had a rubber stamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules
&lt;/h2&gt;

&lt;p&gt;The fix is two clauses, in the order a prosecutor would say them. From &lt;code&gt;engine/catalog.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proof_derives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;limpio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_sin_comentarios&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tablas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;case_tables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;tocadas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_TABLA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limpio&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tocadas&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tablas&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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;motivo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sin_datos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One: the query has to read something.&lt;/strong&gt; A SELECT of literals is not a proof, it's a statement. I match &lt;code&gt;FROM&lt;/code&gt; and &lt;code&gt;JOIN&lt;/code&gt; targets against the tables the case actually ships, so a query that reads some unrelated table doesn't count either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two: the query can't contain, written by hand, the value it claims to demonstrate.&lt;/strong&gt; Filtering by the answer is assuming the conclusion. If the culprit is Ada Ferrán, then &lt;code&gt;WHERE name = 'Ada Ferrán'&lt;/code&gt; proves nothing: you already knew.&lt;/p&gt;

&lt;p&gt;The second rule is where it gets interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I got wrong twice
&lt;/h2&gt;

&lt;p&gt;First attempt: collect every literal in the query, compare against the answer. Fine for strings. Then someone filters by date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;moment&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2019-03-12 21:00'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the answer to "how many visits" is 12, and you scan the raw query text for numbers, that &lt;code&gt;12&lt;/code&gt; inside the date is a hit. A legitimate time filter gets rejected as cheating, which is the worst kind of bug: the player did it right and the game called them a liar.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: look for numbers &lt;em&gt;outside&lt;/em&gt; the strings, since the strings are already compared whole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sin_cadenas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_LITERAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limpio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;literales&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d+(?:\.\d+)?\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sin_cadenas&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second thing is a rule I wrote for a case that doesn't exist yet. The seasons run as a thread, and sooner or later a case will ask for something you legitimately learned three cases ago — an account number, a card. Typing it then isn't cheating, it's remembering. So an answer field can opt out with &lt;code&gt;literal_ok: true&lt;/code&gt;. Nothing uses it today. I'd rather have the escape hatch ready than loosen the rule for everyone the first time a case needs it.&lt;/p&gt;

&lt;p&gt;Both of these came from watching the thing reject queries it shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it in the browser without shipping the answer
&lt;/h2&gt;

&lt;p&gt;The game also runs fully client-side: DuckDB compiled to WebAssembly, no server, nothing to sign up for. Which raises the obvious question — if validation happens on the player's machine, isn't the answer sitting right there in the JavaScript?&lt;/p&gt;

&lt;p&gt;The catalog doesn't carry the answers. It carries plain SHA-256 hashes of the normalized accepted values, and the same two rules run against those:&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;aceptados&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;campo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hashes&lt;/span&gt; &lt;span class="o"&gt;||&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;const&lt;/span&gt; &lt;span class="nx"&gt;bruto&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;textos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;numeros&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;clave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bruto&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clave&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;aceptados&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clave&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;ok&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;motivo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;literal&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;Normalizing first (lowercase, strip accents and punctuation) means "ada ferran" and "Ada Ferrán" hash the same, which is what you want when someone types an answer at 1am without accents.&lt;/p&gt;

&lt;p&gt;I'll be honest about the limits, because this is where blog posts usually oversell: a name is low-entropy, so the hashes are brute-forceable by anyone who wants to, and the full solution file for each case is served as a static asset anyway. This is a single-player game, not an exam. The hashes exist so the answer isn't lying in plain sight in a file anyone opens out of curiosity, not to stop a determined person from cheating at a game they paid for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DuckDB-WASM costs, honestly
&lt;/h2&gt;

&lt;p&gt;The whole engine ships to the browser. Measured on the deployed build: about &lt;strong&gt;8.8 MB&lt;/strong&gt; before you can run your first query, of which 7.3 MB is the gzipped wasm and 626 KB the Parquet extension. On a good connection that's a couple of seconds. On slow mobile data it's closer to 45.&lt;/p&gt;

&lt;p&gt;Three things make it survivable. The title screen needs only ~650 KB, so the game opens immediately. The case file opens without waiting for the engine, and it opens onto text — a briefing, a list of suspects — so there's something to read while the rest arrives. And the download starts before the player asks for it.&lt;/p&gt;

&lt;p&gt;What you get for that weight is worth saying plainly: real SQL. Window functions, CTEs, &lt;code&gt;QUALIFY&lt;/code&gt;, Parquet read directly, actual timestamp semantics. When the game teaches that &lt;code&gt;BETWEEN&lt;/code&gt; on two dates silently drops most of the second day, it's because the engine does exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error messages are game design
&lt;/h2&gt;

&lt;p&gt;This one I learned from a player. They got stuck filtering by hour and wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entry_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'21:00'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversion Error: invalid timestamp field format: "21:00",
expected format is (YYYY-MM-DD HH:MM:SS[.US][±HH[:MM[:SS]]| ZONE])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My game translated that into something friendlier about the format being year-month-day, and "the day doesn't go first". Which is excellent advice for a mistake they hadn't made. They hadn't put the day last, they hadn't put a day at all.&lt;/p&gt;

&lt;p&gt;Now the game recognizes that case specifically and says that a TIMESTAMP holds a date &lt;em&gt;and&lt;/em&gt; a time, and offers both ways out: write the full date, or use &lt;code&gt;hour(entry_time) &amp;gt;= 21&lt;/code&gt;. And it links straight to the field manual entry.&lt;/p&gt;

&lt;p&gt;Which exposed the other half of the bug: that manual entry only unlocked after finishing the case it belonged to. The reference arrived right after you needed it. It now unlocks when you start the case.&lt;/p&gt;

&lt;p&gt;They asked one question in a comment thread and it produced two fixes. That's a better rate than any of my own playtesting.&lt;/p&gt;




&lt;p&gt;The game is &lt;a href="https://caso-abierto.christianvadillo.workers.dev" rel="noopener noreferrer"&gt;Caso Abierto&lt;/a&gt;. The first case runs in your browser, free, no signup — if you want to see the proof rule reject you, &lt;code&gt;SELECT 'Ada Ferrán', '22:20'&lt;/code&gt; is still the fastest way.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>duckdb</category>
      <category>webassembly</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
