<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://blog.mathieuacher.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://blog.mathieuacher.com/" rel="alternate" type="text/html" /><updated>2026-09-28T05:30:15+00:00</updated><id>http://blog.mathieuacher.com/feed.xml</id><title type="html">Mathieu Acher</title><subtitle>Professor in Computer Science</subtitle><entry><title type="html">Do Linus Torvalds and Greg Kroah-Hartman Still “Sound” Different on the Linux Kernel Mailing List?</title><link href="http://blog.mathieuacher.com/LKMLLinusGregCommunicationStyleReplication/" rel="alternate" type="text/html" title="Do Linus Torvalds and Greg Kroah-Hartman Still “Sound” Different on the Linux Kernel Mailing List?" /><published>2026-09-22T12:00:00+00:00</published><updated>2026-09-22T12:00:00+00:00</updated><id>http://blog.mathieuacher.com/LKMLLinusGregCommunicationStyleReplication</id><content type="html" xml:base="http://blog.mathieuacher.com/LKMLLinusGregCommunicationStyleReplication/"><![CDATA[<p>In our VEM 2026 (Workshop on Software Visualization, Maintenance and Evolution, co-located with CBSoft 2026) paper, <em>A Replication Study of Communication Style Analysis in the Linux Kernel Mailing List</em>, we rerun a classic 2016 analysis (Schneider, Spurlock &amp; Squire, <em>Differentiating Communication Styles of Leaders on the Linux Kernel Mailing List</em>) on a modern dataset. Short answer: yes, they still sound different.</p>

<p>The original study showed that a classifier could tell the two kernel leaders apart just from how they write. We re-executed the same pipeline, Python 2.7 and all, on LKML5Ws, a curated dataset of 20M+ emails from 345 kernel mailing lists.</p>

<h2 id="what-we-found">What we found</h2>

<p>📦 <strong>“The data is public” ≠ “the data is usable.”</strong> Rebuilding the corpus from <a href="https://lore.kernel.org/">lore.kernel.org</a> with <code class="language-plaintext highlighter-rouge">lei</code> worked for small time ranges but broke down at full-history scale (message bodies, thread completeness). So we switched to a curated dataset.</p>

<p>📊 <strong>Classification.</strong> On a balanced corpus of ~65.8k messages, Naive Bayes reaches F1 92.7% / accuracy 93.0% (original: 96.3% / 96.2%). Lower, but the stylistic signal clearly survives a different corpus.</p>

<p>✍️ <strong>Style.</strong> Linus writes shorter messages (279 vs 524 words), with more varied vocabulary and far more adverbs (5.2% vs 1.3%). His telltale words: <em>just</em>, <em>really</em>, <em>actually</em>, <em>think</em>. Greg KH’s: <em>drivers</em>, <em>signed</em>, <em>struct</em>, <em>static</em>. Conversational vs. technical ;-)</p>

<p>🤬 <strong>Expletives.</strong> Linus still leads (4,653 vs 3,041), but raw counts mislead. String matching flags surnames like “wang”, “cox” and “willy” as profanity, inflating the counts (these are real contributors!). Filter those out and the gap is stark: “damn” appears 951 times for Linus vs 12 for Greg.</p>

<p><img src="/assets/lkml-expletives-linus-greg.png" alt="Horizontal bar chart of the most frequent unambiguous expletive terms for Linus Torvalds and Greg Kroah-Hartman: damn 951 vs 12, hell 933 vs 20, crap 795 vs 49, shit 152 vs 0, anal 57 vs 0, butt 0 vs 100, knob 0 vs 31" style="max-width: 100%;" /></p>

<p><em>Most frequent expletive terms without ambiguities (Figure 2 of the paper).</em></p>

<p>⚠️ <strong>Caveats.</strong> Some messages lack timestamps, so no temporal analysis yet. Next for us: LLM-based detection, longitudinal analysis, sampling sensitivity.</p>

<h2 id="credits">Credits</h2>

<p>Great job by Júlia Azevedo during her stay at INSA Rennes, together with Theo Canuto, Heraldo Borges and Juliana Alves Pereira from PUC-Rio.</p>

<ul>
  <li>📄 Preprint: <a href="https://inria.hal.science/hal-05756009">hal-05756009</a></li>
  <li>💻 Artifacts: <a href="https://github.com/aisepucrio/LKML-VEM2026">github.com/aisepucrio/LKML-VEM2026</a></li>
</ul>]]></content><author><name></name></author><category term="Linux kernel" /><category term="LKML" /><category term="replication" /><category term="empirical software engineering" /><category term="mining software repositories" /><category term="NLP" /><category term="Naive Bayes" /><category term="communication style" /><category term="open source" /><category term="VEM 2026" /><category term="reproducibility" /><summary type="html"><![CDATA[In our VEM 2026 (Workshop on Software Visualization, Maintenance and Evolution, co-located with CBSoft 2026) paper, A Replication Study of Communication Style Analysis in the Linux Kernel Mailing List, we rerun a classic 2016 analysis (Schneider, Spurlock &amp; Squire, Differentiating Communication Styles of Leaders on the Linux Kernel Mailing List) on a modern dataset. Short answer: yes, they still sound different.]]></summary></entry><entry><title type="html">Langages de programmation à l’ère de l’IA générative : COBOL, M&amp;amp;M’s, français… ou peu importe ?</title><link href="http://blog.mathieuacher.com/LangagesProgrammationIAGenerativeSemaineIA2026/" rel="alternate" type="text/html" title="Langages de programmation à l’ère de l’IA générative : COBOL, M&amp;amp;M’s, français… ou peu importe ?" /><published>2026-09-07T10:00:00+00:00</published><updated>2026-09-07T10:00:00+00:00</updated><id>http://blog.mathieuacher.com/LangagesProgrammationIAGenerativeSemaineIA2026</id><content type="html" xml:base="http://blog.mathieuacher.com/LangagesProgrammationIAGenerativeSemaineIA2026/"><![CDATA[<p>Vendredi 4 septembre 2026, j’ai donné un exposé dans le cadre de la <a href="https://intelligence-artificielle.univ-rennes.fr/semaine-ia-2026">Semaine de l’IA de l’Université de Rennes</a>, devant quelque 230 étudiantes et étudiants (en cœur IA ou coloration IA), sur ce que les agents de codage font des langages de programmation, de Python à COBOL en passant par Brainfuck et les M&amp;M’s. L’enregistrement est en ligne, ainsi que les slides, en français et en anglais.</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/Aj5arTQzMQ4?start=822" title="Langages de programmation à l'ère de l'IA générative — Semaine de l'IA 2026" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<ul>
  <li><strong>Vidéo de l’exposé</strong> : <a href="https://www.youtube.com/watch?v=Aj5arTQzMQ4&amp;t=822s">youtube.com/watch?v=Aj5arTQzMQ4</a> (l’exposé commence à 13 min 42 s)</li>
  <li><strong>Slides en français</strong> : <a href="https://blog.mathieuacher.com/languages-codingagents-semaineIA2026/">blog.mathieuacher.com/languages-codingagents-semaineIA2026</a></li>
  <li><strong>Slides in English</strong>: <a href="https://blog.mathieuacher.com/languages-codingagents-semaineIA2026/en.html">blog.mathieuacher.com/languages-codingagents-semaineIA2026/en.html</a></li>
  <li><strong>Sources</strong> (reveal.js, vidéos et figures comprises) : <a href="https://github.com/acherm/languages-codingagents-semaineIA2026">github.com/acherm/languages-codingagents-semaineIA2026</a></li>
</ul>

<p>Les slides sont auto-contenues : vidéos des jeux, extraits de console, figures des papiers, tout est dedans. Touche <code class="language-plaintext highlighter-rouge">S</code> pour les notes, <code class="language-plaintext highlighter-rouge">O</code> pour la vue d’ensemble. Une trentaine de slides de « backup » suivent les références, pour la discussion.</p>

<h2 id="résumé">Résumé</h2>

<p>À l’ère de l’IA générative et du <em>vibe coding</em>, programmer peut commencer par une simple description en langue naturelle, en anglais ou en français, de ce que l’on souhaite obtenir. Des agents de codage peuvent aujourd’hui produire des sites web, des jeux, des solveurs, voire des compilateurs ou des outils plus ambitieux, avec des résultats parfois impressionnants. Pour tester leurs limites, on peut les confronter à des langages très différents : Python, Rust ou JavaScript, mais aussi COBOL, l’un des plus vieux langages encore utilisés, Brainfuck, un langage composé de seulement 8 caractères, ou même un langage à base de M&amp;Ms.</p>

<p>Ces expériences donnent lieu à des démonstrations étonnantes : un jeu DOOM en COBOL, un jeu d’échecs en Brainfuck ou des programmes non triviaux écrits avec des bonbons colorés. L’intérêt est que ces programmes ne sont pas de simples copies retrouvées quelque part : ils sont générés à chaque fois, avec leurs propres choix, leurs surprises, leurs réussites et leurs défauts. Car tout ne fonctionne pas toujours : le programme peut être incorrect, fragile, incomplet, ou faire ce qui est demandé de manière très maladroite (par exemple jouer très mal aux échecs).</p>

<p>Derrière le spectacle, une question demeure : les langages de programmation vont-ils disparaître, ou devenir au contraire encore plus importants pour préciser, contrôler et comprendre ce que l’IA produit ?</p>

<h2 id="pour-aller-plus-loin">Pour aller plus loin</h2>

<p>Les expériences de l’exposé sont détaillées dans des billets de ce blog et dans deux études :</p>

<ul>
  <li><a href="https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/">CallOfAcher et FIFAcher</a> : vibe coder des variantes de jeux avec mon neveu de 10 ans (jouables sur <a href="https://blog.mathieuacher.com/ashfall/">ashfall</a> et <a href="https://blog.mathieuacher.com/fifacher/">fifacher</a>)</li>
  <li><a href="https://blog.mathieuacher.com/BFChessChessEngineBrainfuck/">BFChess</a> : un moteur d’échecs en Brainfuck</li>
  <li><a href="https://blog.mathieuacher.com/CodingAgentsMnMLang/">MnM Lang</a> : des programmes en M&amp;M’s, et le <a href="https://github.com/acherm/agentic-arnoldc">triple interpréteur ArnoldC → MnM → Brainfuck</a></li>
  <li><a href="https://blog.mathieuacher.com/TeXCCChessEngine/">TeXCCChess</a> : jouer aux échecs dans Overleaf</li>
  <li><a href="https://blog.mathieuacher.com/PL-ultimate-llm/">PL Catalog</a> : 14 000+ langages de programmation, avec Software Heritage et CodeCommons</li>
  <li>Mathieu Acher, Jean-Marc Jézéquel, <a href="https://arxiv.org/abs/2606.13763">Do Programming Languages Still Matter to Your AI Coding Agent Teammate? Evidence at Scale from Chess Engines</a>, 2026 : 34 moteurs d’échecs, 17 langages, 2 agents</li>
  <li>Mathieu Acher, Zohra Kaouter Kebaili, Jean-Baptiste Espinasse, Djamel Eddine Khelladi, Coding Agents Develop COBOL Systems: Evidence from a Human-Guided Multi-Case Study, 2026 : 16 systèmes COBOL créés de zéro, dont <a href="https://github.com/acherm/agentic-cobol-study">DOOM</a> https://inria.hal.science/hal-05743207/</li>
</ul>

<p>Merci aux organisatrices et organisateurs de la Semaine de l’IA, et au public pour les questions !</p>

<p><em>Un avis, une idée, un bug trouvé ? mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026langagesIAgenerative</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{Langages de programmation à l'ère de l'IA générative : COBOL, M\&amp;M's, français… ou peu importe ?}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{sep}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{Exposé, Semaine de l'IA, Université de Rennes, 4 septembre 2026. \url{https://blog.mathieuacher.com/LangagesProgrammationIAGenerativeSemaineIA2026/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{Vidéo : \url{https://www.youtube.com/watch?v=Aj5arTQzMQ4}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="LLM" /><category term="coding agents" /><category term="Claude Code" /><category term="generative AI" /><category term="llm4code" /><category term="programming languages" /><category term="COBOL" /><category term="Brainfuck" /><category term="esoteric languages" /><category term="chess" /><category term="vibe coding" /><category term="talk" /><category term="Semaine de l&apos;IA" /><summary type="html"><![CDATA[Vendredi 4 septembre 2026, j’ai donné un exposé dans le cadre de la Semaine de l’IA de l’Université de Rennes, devant quelque 230 étudiantes et étudiants (en cœur IA ou coloration IA), sur ce que les agents de codage font des langages de programmation, de Python à COBOL en passant par Brainfuck et les M&amp;M’s. L’enregistrement est en ligne, ainsi que les slides, en français et en anglais.]]></summary></entry><entry><title type="html">CallOfAcher and FIFAcher: Vibe Coding Game Variants with My 10-Year-Old Nephew</title><link href="http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIAEN/" rel="alternate" type="text/html" title="CallOfAcher and FIFAcher: Vibe Coding Game Variants with My 10-Year-Old Nephew" /><published>2026-08-06T07:00:00+00:00</published><updated>2026-08-06T07:00:00+00:00</updated><id>http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIAEN</id><content type="html" xml:base="http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIAEN/"><![CDATA[<p>A week ago, I pasted <a href="https://x.com/mattshumer_/status/2081100590612922527">this prompt by Matt Shumer</a> (150 words, not a single line of technical spec) into <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>, and went off to do something else. When I came back: a playable FPS in the browser. Not a demo, not a prototype. A game. I did it again two days later, replacing “Call of Duty” with “FIFA”, and that worked too. Then my 10-year-old nephew took over and grew both games, coming up with ideas, typing what he wanted straight into Claude’s terminal, playing, criticizing, and so on, right up to today.
Both games are online, playable right now (even on a phone!), with nothing to install:</p>
<ul>
  <li><strong><a href="https://blog.mathieuacher.com/ashfall/">blog.mathieuacher.com/ashfall</a></strong>: the FPS, with its two-player co-op mode, its environments, and so on. (<a href="https://www.youtube.com/shorts/DUihik38JEA">video demo</a>)</li>
  <li><strong><a href="https://blog.mathieuacher.com/fifacher/">blog.mathieuacher.com/fifacher</a></strong>: the football game, a match at Stade Raoul Brulat with cicadas and two exceptional players. (<a href="https://www.youtube.com/shorts/CMiOrjE6TMY">video demo</a>)</li>
</ul>

<p>You click, it runs. On a phone, the touch joysticks appear on their own. On a computer it is keyboard and mouse, and if you plug in a controller (a Switch pad over USB or Bluetooth, or any standard gamepad), it is recognized immediately. Nothing to download, nothing to compile, no account to create.</p>

<p>What came after the first shot interests me more than the first shot itself. And one clarification that holds for the rest of this post: apart from that initial prompt, <strong>everything was done with my 10-year-old nephew</strong>, who knows nothing about programming. The numbers below, the motorbikes, the extra enemies, the multiplayer mode, the Breton environments: that is a week spent side by side at the console. He understood the principle in ten minutes. Here is what I take away from it, with numbers to back it up.</p>

<p><img src="/assets/callofacher-ballon-cr7.jpg" alt="CallOfAcher: first-person view in a ruined city, a football in hand, a CR7 in a red shirt taking cover behind a car" style="max-width: 100%;" /></p>

<p><em>CallOfAcher. The objective shown in the top left, “MARQUEZ CONTRE LES RONALDO” (score against the Ronaldos), is my nephew’s idea: you don’t fire bullets, you fire footballs at CR7s. Everything else, the ruined city, the compass, the HUD, the vehicles, is generated in code. <a href="https://www.youtube.com/shorts/DUihik38JEA">Video demo</a>.</em></p>

<p><img src="/assets/fifacher-stade-raoul-brulat.jpg" alt="FIFAcher: night match at Stade Raoul Brulat, full stands, LED advertising boards, commentary line at the bottom of the screen" style="max-width: 100%;" /></p>

<p><em>FIFAcher, at Stade Raoul Brulat. The crowd in the stands is a single render object. The sponsors on the LED boards (Breizh Cola, Kroaz Air, Menhir Bank, Armor Telecom) were invented by the model, which preferred a fictional Breton league to real brands. At the bottom, the live commentary reacts to the fact that two Achers are on the same team. <a href="https://www.youtube.com/shorts/CMiOrjE6TMY">Video demo</a>.</em></p>

<h2 id="one-prompt-one-game">One Prompt, One Game</h2>

<p>The prompt fits in a single paragraph. It doesn’t say <em>how</em> to do it, it says <em>what we want</em> and <em>how to check</em>:</p>

<blockquote>
  <p>I want you to build a first-person shooter at the level of the most recent Call of Duty games. […] Fan out sub-agents and have sub-agents tackle each one individually […] have a separate sub-agent check it visually to ensure it looks triple A. That separate sub-agent should be a really harsh critic […] Do this in ThreeJS. /loop until it’s utterly perfect.</p>
</blockquote>

<p>For FIFAcher, I changed three words. That’s all. And the first shot is playable: you move, you shoot, it runs at 60 fps.</p>

<h2 id="what-it-sets-in-motion">What It Sets in Motion</h2>

<p>This is where I wanted to look under the hood. I analyzed the session logs, the commits, and the sub-agent transcripts. The number that surprised me most is not the line count, it is the agent count.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>CallOfAcher (ASHFALL)</th>
      <th>FIFAcher</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Working window</td>
      <td>July 26 → 31</td>
      <td>July 28 → 31</td>
    </tr>
    <tr>
      <td>Requests we typed</td>
      <td>45</td>
      <td>15</td>
    </tr>
    <tr>
      <td>Sub-agents launched</td>
      <td>56</td>
      <td>36</td>
    </tr>
    <tr>
      <td>Tokens processed</td>
      <td>585M</td>
      <td>562M</td>
    </tr>
    <tr>
      <td>Tokens generated</td>
      <td>1.03M</td>
      <td>1.15M</td>
    </tr>
    <tr>
      <td>Session logs</td>
      <td>465 MB</td>
      <td>462 MB</td>
    </tr>
    <tr>
      <td>Commits</td>
      <td>14</td>
      <td>12</td>
    </tr>
  </tbody>
</table>

<p>In total: <strong>92 sub-agents</strong>, <strong>1.15 billion tokens processed</strong>, <strong>2.18 million tokens generated</strong>, for <strong>60 requests</strong> typed by us. The ratio is dizzying: one sentence in French triggers hours of machine work. Three models took turns depending on the phase (Opus 4.8, Opus 5, Fable 5).</p>

<p>The “us” is literal: of those 60 requests, only two are the initial prompts. Everything else was formulated together, and a good share of it typed directly by my nephew. So these numbers do not describe the performance of the machine alone, but what a week of conversation between two people, one of them a 10-year-old, manages to set in motion.</p>

<p>The orchestration pattern deserves a word. For FIFAcher, the initial build was a workflow of 10 agents in parallel, one per subsystem (crowd, pitch, ball, lighting, audio commentary, post-processing, and so on), each writing its module against a shared interface contract: a 14 KB <code class="language-plaintext highlighter-rouge">CONTRACT.md</code> file generated beforehand. Two hours and forty minutes of wall clock, ten concurrent agents. Then rounds of “harsh critics” that take screenshots and score the result, until the scores plateau.</p>

<p>That visual verification loop is the heart of the setup, and it is <em>laborious</em>. ASHFALL left <strong>199 screenshots</strong> in the repository, FIFAcher 31, plus 37 small verification scripts (<code class="language-plaintext highlighter-rouge">voicecheck.mjs</code>, <code class="language-plaintext highlighter-rouge">switchcheck.mjs</code>, <code class="language-plaintext highlighter-rouge">rendercost.mjs</code>, and others) that the agent wrote to inspect itself. It doesn’t <em>see</em> the game running: it photographs it, frame by frame, and reasons about the photos.</p>

<h2 id="the-cost">The Cost</h2>

<p>I reconstructed it from the logs, at public API rates: <strong>between $1,000 and $1,200</strong> for both games (the range depends on the cache TTL applied). It is a reconstruction, not an invoice, since I was on a subscription, but the order of magnitude is right.</p>

<p>The most instructive part is not the amount, it is <em>where</em> the money goes. Certainly not into the code produced. Out of 1.15 billion tokens, the generated code accounts for only <strong>2.18 million tokens, that is 0.19% of the volume</strong>. The remaining 99.8% is <strong>context</strong>: on every call, the agent has the game it is modifying explained to it all over again. Across 5,295 API calls, that averages <strong>216,000 tokens of context per call</strong>, read over and over.</p>

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Volume</th>
      <th>Share of cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Reading context (cache)</td>
      <td>1.11B tokens</td>
      <td>55%</td>
    </tr>
    <tr>
      <td>Writing context (cache)</td>
      <td>36.3M tokens</td>
      <td>39%</td>
    </tr>
    <tr>
      <td><strong>Code actually produced</strong></td>
      <td><strong>2.18M tokens</strong></td>
      <td><strong>6%</strong></td>
    </tr>
  </tbody>
</table>

<p>In other words: <strong>94% of what you pay is reading and re-reading the thousands of existing lines, and only 6% is the code written</strong>. That is where the real cost lies, and it is structural. The bigger the game gets, the more expensive the slightest change becomes, including a trivial one. Adding a motorbike on day five costs far more than adding the same motorbike on day two, not because the motorbike is more complicated, but because you have to traverse everything that already exists to put it in the right place. You are not paying for the feature, you are paying for the context it forces you to walk through. A 200-line project can be tinkered with for a few cents; the same gesture on 19,000 lines costs a hundred times more.</p>

<p>And that is the painful part. It is not within everyone’s reach. You also need frontier models, presumably: I doubt a model two notches down would hold a 92-agent loop together without going off the rails. That said, I am very confident about democratization. In a year, those $1,000 will be worth $50. Even today, you can already cut the cost drastically by combining lighter, specialized models with frontier models, or by using your technical expertise to constrain the agentic system more tightly. Given the breakdown above, context is in fact where to act first: split the modules better, target more precisely what you hand the agent to read, and don’t let every sub-agent swallow the whole project.</p>

<h2 id="everything-else-was-done-together">Everything Else Was Done Together</h2>

<p>This is the part I hadn’t anticipated. Once the game exists, extending it becomes a conversation. My nephew asks, I barely rephrase, he even types directly into the console, and the agent codes. In a week, we added:</p>

<ul>
  <li><strong>firing a football</strong> at enemies (with, it must be said, a “SIUU” celebration cry on the V key);</li>
  <li>drivable <strong>cars and motorbikes</strong>;</li>
  <li>more enemies, civilians who flee from gunfire;</li>
  <li><strong>real environments</strong>: the Vallée de Gandy, the Stade Vélodrome, and the village of Saint-Aubin-du-Cormier with its pond and its castle ruins;</li>
  <li><strong>difficulty settings</strong>;</li>
  <li><strong>controller</strong> support, almost immediate, thanks to the Web’s Gamepad API;</li>
  <li>a <strong>mobile mode</strong> with touch joysticks;</li>
  <li>a two-player <strong>online co-op mode</strong>, in <strong>peer-to-peer WebRTC, with no server</strong>: a 4-character code, and you play together.</li>
</ul>

<p>The TL;DR: it is <em>easy</em>. A 10-year-old can do it and grasps the principle very quickly. Yes, the waiting times are long, but meanwhile you play actual football, board games, you go for a swim, you live, and you come back whenever you feel like it. The development cycle follows the rhythm of the holidays, which is a sentence I never imagined writing.</p>

<h2 id="the-stack-or-why-the-web-changes-everything">The Stack, or Why the Web Changes Everything</h2>

<p>Technically, it is disarmingly simple, and that is precisely the point:</p>

<ul>
  <li><strong>Three.js</strong> (r185) and <strong>nothing else</strong> on the rendering side. No external assets: every texture, every model, every sound is <strong>generated in code</strong>. FIFAcher’s stadium crowd is tens of thousands of supporters in a single <code class="language-plaintext highlighter-rouge">InstancedMesh</code>, hence a single draw call, with a 64-cell atlas painted procedurally onto a canvas, and a separate mask so that tinting only ever touches clothing and never skin.</li>
  <li>~18,900 lines of JavaScript for ASHFALL, spread across 19 ES modules.</li>
  <li><strong>Vite</strong> for the build, <strong>Playwright/Puppeteer</strong> for automatic screenshots.</li>
  <li><strong>PeerJS/WebRTC</strong> for co-op, with the free broker used <em>only</em> for signalling.</li>
  <li><strong>Static deployment on GitHub Pages</strong> via GitHub Actions. Zero servers, zero infra, zero hosting cost.</li>
</ul>

<p>It is that last point that makes the whole thing real rather than anecdotal. One <code class="language-plaintext highlighter-rouge">git push</code>, and the game is online, for anyone, anywhere, immediately. No store, no review, no per-platform build. The same <code class="language-plaintext highlighter-rouge">index.html</code> file serves the kid on his phone on the train and the adult with a controller in front of a screen. Go click the two links at the top, it is faster than finishing this paragraph.</p>

<h2 id="what-doesnt-work">What Doesn’t Work</h2>

<p>Let’s be clear: <strong>this is not the level of a real Call of Duty or a real FIFA</strong>. The gap is real, and it has several causes.</p>

<p><strong>Graphics and 3D.</strong> This is where the gap is most visible. Procedural work without an artist shows.</p>

<p><strong>Instructions sometimes ignored.</strong> Sometimes a requested feature simply isn’t implemented, with nothing to flag it. You have to ask again.</p>

<p><strong>Verification stays superficial.</strong> The critic-agents score screenshots, they don’t play. The result: bugs and glitches all over the place, including things a human spots in three seconds with a controller.</p>

<p><strong>Playability and tuning: hard, really hard.</strong> This is the hardest point. Adjusting sensitivity, balance, the “feel”, takes dozens of back-and-forths, and it is the area where the agent is most helpless.</p>

<p><strong>Knowledge of real places is genuine but poorly rendered.</strong> On the Vélodrome, the model <em>knows</em> accurate things: the shape of the stands, the atmosphere. But the rendering stays approximate. On other environments, despite obvious knowledge, the result is frankly bullshit. Much more work is needed.</p>

<p>That said, an amusing nuance: for FIFAcher, the technical weakness <strong>almost becomes a feature</strong>. The game doesn’t play like a FIFA, it forces you to find other strategies. The gameplay is different, not merely inferior.</p>

<h2 id="the-real-ceiling">The Real Ceiling</h2>

<p><a href="https://x.com/karpathy/status/2083749667410727319">Karpathy posted</a> a reflection that overlaps exactly with what I observed. Two ideas struck me.</p>

<p>The first: we are leaving the territory of “draw me a pelican in SVG”. These games belong to a category of tasks that <strong>nobody in their right mind would have undertaken</strong>, too custom, too long, no return on investment. LLMs have all the patience in the world, so we go from “nobody would ever do this” to “why not, it’s basically free”. Hyper-personalized worlds on demand. That is exactly our experience: Saint-Aubin-du-Cormier as a video game did not exist and never would have.</p>

<p>The second, harsher, explains my bugs: <strong>models cannot audit their own work</strong> in this domain, because they do not natively perceive video and do not play the game. My 199 screenshots are the material proof of that handicap: a slow and painful verification loop, where a human with a controller settles it in five seconds.</p>

<p>This is not just an impression. We measured the phenomenon in a <a href="https://arxiv.org/abs/2606.15693">recent paper on imperfect visual verification</a>, using TikZ as a case study: how far does iterative refinement remain effective when the verifier itself is unreliable? Visual verifiers, whether LLM-based or tool-augmented, do not exceed an <strong>F1 of 0.815</strong> when determining whether a visual instruction has actually been applied to the code. In other words: in roughly one case out of five, the “generate, look, fix” loop is wrong about its own diagnosis. We also observe that feedback only really helps if it precisely identifies the flaw in the image and proposes an actionable correction, otherwise it adds noise. And if TikZ, which produces static 2D figures, already poses this problem, you can imagine the size of the gap on an animated 3D world that would have to be <em>played</em> to be evaluated.</p>

<p>As long as that lock holds, the gap with a real AAA will remain.</p>

<h2 id="the-wow-and-the-open-problems">The “Wow”, and the Open Problems</h2>

<p><strong>Anyone can create their own game.</strong> That is the incredible part. Player names, teams, stadium, setting: everything is customizable. In FIFAcher, the home team is made up of eleven Achers, first names on the shirts, each with their own hair colour. Gone are the days when add-ons were reserved for hackers.</p>

<p><strong>Copyright.</strong> I asked to incorporate licensed audio (Jump, for the players walking out): a flat refusal, and rightly so. But the boundary is blurry. What stops me from putting the real player names in? The model, for its part, drew its own line without being asked: it designed <em>original</em> crests in the clubs’ colours, explicitly writing in the README that the real badges are not reproduced. Learned caution, not an explicit rule.</p>

<p><strong>Multimodality opens up what comes next.</strong> Plenty of open source components exist for 3D modelling. Nothing prevents building real little workshops to seriously model the environments, the players, the stadiums, with AI of course. That is the missing link on graphics.</p>

<p><strong>Can an “open” FIFA emerge?</strong> Technically, the building block is there. But I am not so sure, and for a social rather than technical reason: can a shared <em>platform</em> emerge when anyone can walk away with their own variant? Customization taken to the extreme may be the enemy of critical mass.</p>

<p><strong>The AI / game-dev boundary.</strong> We developed in the console. Yes, really. And we clearly wanted to edit the game <em>inside</em> the game. The next ergonomic leap may well be there.</p>

<p><strong>We never looked at the code.</strong> Never talked about the code either, and just as well: that is what made collaborating with a 10-year-old possible. But you do need experience to steer the model back when it drifts, to deploy, to understand why it lags. Not much more, at this level of complexity. The famous gap will show up further along: when complexity and expectations rise (a real AAA), you will have to reclaim some of the technical ground.</p>

<p>That leaves the simplest thing, and it may be the real argument. Both games are there, at a URL, free, with no installation: <strong><a href="https://blog.mathieuacher.com/ashfall/">ashfall</a></strong> and <strong><a href="https://blog.mathieuacher.com/fifacher/">fifacher</a></strong>. Grab a controller, or your phone, and play for five minutes. You will see the seams, there are many. But you will be playing something nobody would ever have taken the time to build, written in a week of holidays by a researcher and a 10-year-old who has not read a single line of code. That is the news. And it is far from finished moving.</p>

<hr />

<p><em>Both games: <a href="https://blog.mathieuacher.com/ashfall/">ashfall</a> (<a href="https://github.com/acherm/ashfall">code</a>, <a href="https://www.youtube.com/shorts/DUihik38JEA">video demo</a>) and <a href="https://blog.mathieuacher.com/fifacher/">fifacher</a> (<a href="https://github.com/acherm/fifacher">code</a>, <a href="https://www.youtube.com/shorts/CMiOrjE6TMY">video demo</a>). Initial prompt by <a href="https://x.com/mattshumer_/status/2081100590612922527">Matt Shumer</a> (<a href="https://github.com/mshumer/Claude-of-Duty/blob/main/prompt.md">repository</a>), adapted for football.</em></p>

<p><em>Methodology for the numbers: reconstruction from Claude Code session transcripts (927 MB of JSONL). Requests are deduplicated by message identifier, and resumed sessions (which replay the previous session’s history) are merged so nothing is counted twice. The “requests” count only what we actually wrote: automatic task-completion notifications, command echoes, and “continue” messages are excluded. The cost is estimated at public API rates (input, output, cache write and cache read), not taken from an invoice. Models: Claude Opus 4.8, Opus 5, and Fable 5.</em></p>

<p><em>On visual verification: Charly Reux, Mathieu Acher, Djamel Eddine Khelladi, Clément Quinton, Olivier Barais, <a href="https://arxiv.org/abs/2606.15693">Imperfect Visual Verification for Code Edition: A Case Study on TikZ</a>, 2026.</em></p>

<p><em>This post is the English version of one originally published in French: <a href="https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/">CallOfAcher et FIFAcher : vibe coder des variantes de jeux avec mon neveu de 10 ans</a>.</em></p>

<p><em>A comment, an idea, a bug found? mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026callofacheren</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{CallOfAcher and FIFAcher: Vibe Coding Game Variants with My 10-Year-Old Nephew}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{aug}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIAEN/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIAEN/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="LLM" /><category term="coding agents" /><category term="Claude Code" /><category term="Three.js" /><category term="video games" /><category term="generative AI" /><category term="llm4code" /><category term="WebRTC" /><category term="GitHub Pages" /><category term="sub-agents" /><category term="vibe coding" /><summary type="html"><![CDATA[A week ago, I pasted this prompt by Matt Shumer (150 words, not a single line of technical spec) into Claude Code, and went off to do something else. When I came back: a playable FPS in the browser. Not a demo, not a prototype. A game. I did it again two days later, replacing “Call of Duty” with “FIFA”, and that worked too. Then my 10-year-old nephew took over and grew both games, coming up with ideas, typing what he wanted straight into Claude’s terminal, playing, criticizing, and so on, right up to today. Both games are online, playable right now (even on a phone!), with nothing to install: blog.mathieuacher.com/ashfall: the FPS, with its two-player co-op mode, its environments, and so on. (video demo) blog.mathieuacher.com/fifacher: the football game, a match at Stade Raoul Brulat with cicadas and two exceptional players. (video demo)]]></summary></entry><entry><title type="html">CallOfAcher et FIFAcher : vibe coder des variantes de jeux avec mon neveu de 10 ans</title><link href="http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/" rel="alternate" type="text/html" title="CallOfAcher et FIFAcher : vibe coder des variantes de jeux avec mon neveu de 10 ans" /><published>2026-08-03T07:00:00+00:00</published><updated>2026-08-03T07:00:00+00:00</updated><id>http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA</id><content type="html" xml:base="http://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/"><![CDATA[<p>Il y a une semaine, j’ai collé <a href="https://x.com/mattshumer_/status/2081100590612922527">ce prompt de Matt Shumer</a> (150 mots, pas une ligne de spec technique) dans <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>, et je suis allé faire autre chose. En rentrant : un FPS jouable dans le navigateur. Pas une démo, pas un prototype. Un jeu. J’ai recommencé deux jours plus tard en remplaçant “Call of Duty” par “FIFA”, et ça a marché aussi. Puis mon neveu de 10 ans a fait évoluer les deux jeux, en donnant des idées, en écrivant directement ce qu’il voulait dans le terminal de Claude, en jouant, en critiquant, et ainsi de suite, jusqu’à aujourd’hui.
Les deux jeux sont en ligne, jouables tout de suite (même sur téléphone !), sans installer quoi que ce soit :</p>
<ul>
  <li><strong><a href="https://blog.mathieuacher.com/ashfall/">blog.mathieuacher.com/ashfall</a></strong> : le FPS, avec son mode coopératif à deux, ses décors, etc. (<a href="https://www.youtube.com/shorts/DUihik38JEA">démo vidéo</a>)</li>
  <li><strong><a href="https://blog.mathieuacher.com/fifacher/">blog.mathieuacher.com/fifacher</a></strong> : le foot, un match au Stade Raoul Brulat avec les cigales et deux joueurs d’exception. (<a href="https://www.youtube.com/shorts/CMiOrjE6TMY">démo vidéo</a>)</li>
</ul>

<p>Vous cliquez, ça se lance. Sur téléphone, les joysticks tactiles apparaissent tout seuls. Sur ordinateur, c’est clavier-souris, et si vous branchez une manette (Switch en USB ou en Bluetooth, ou n’importe quel pad standard), elle est reconnue immédiatement. Rien à télécharger, rien à compiler, aucun compte à créer.</p>

<p>Ce qui a suivi le premier jet m’intéresse davantage que le premier jet lui-même. Et une précision qui vaut pour tout le reste de ce billet : à l’exception de ce prompt initial, <strong>tout a été fait avec mon neveu de 10 ans</strong>, qui ne connaît rien à la programmation. Les chiffres plus bas, les motos, les ennemis en plus, le mode multijoueur, les décors bretons : c’est une semaine à deux devant la console. Il a compris le principe en dix minutes. Voici ce que j’en retiens, chiffres à l’appui.</p>

<p><img src="/assets/callofacher-ballon-cr7.jpg" alt="CallOfAcher : vue à la première personne dans une ville en ruines, un ballon de football en main, un CR7 en maillot rouge à couvert derrière une voiture" style="max-width: 100%;" /></p>

<p><em>CallOfAcher. L’objectif affiché en haut à gauche, “MARQUEZ CONTRE LES RONALDO”, est une idée de mon neveu : on ne tire pas des balles, on tire des ballons de foot sur des CR7. Tout le reste, la ville en ruines, la boussole, le HUD, les véhicules, est généré en code. <a href="https://www.youtube.com/shorts/DUihik38JEA">Démo vidéo</a>.</em></p>

<p><img src="/assets/fifacher-stade-raoul-brulat.jpg" alt="FIFAcher : match de nuit au Stade Raoul Brulat, tribunes pleines, panneaux publicitaires LED, ligne de commentaire en bas de l'écran" style="max-width: 100%;" /></p>

<p><em>FIFAcher, au Stade Raoul Brulat. La foule des tribunes est un seul objet de rendu. Les sponsors sur les panneaux LED (Breizh Cola, Kroaz Air, Menhir Bank, Armor Telecom) ont été inventés par le modèle, qui a préféré une ligue bretonne fictive aux vraies marques. En bas, le commentaire en direct réagit au fait que deux Acher se trouvent dans la même équipe. <a href="https://www.youtube.com/shorts/CMiOrjE6TMY">Démo vidéo</a>.</em></p>

<h2 id="un-prompt-un-jeu">Un prompt, un jeu</h2>

<p>Le prompt tient en un paragraphe. Il ne dit pas <em>comment</em> faire, il dit <em>ce qu’on veut</em> et <em>comment vérifier</em> :</p>

<blockquote>
  <p>I want you to build a first-person shooter at the level of the most recent Call of Duty games. […] Fan out sub-agents and have sub-agents tackle each one individually […] have a separate sub-agent check it visually to ensure it looks triple A. That separate sub-agent should be a really harsh critic […] Do this in ThreeJS. /loop until it’s utterly perfect.</p>
</blockquote>

<p>Pour FIFAcher, j’ai changé trois mots. C’est tout. Et le premier jet est jouable : on se déplace, on tire, ça tourne à 60 fps.</p>

<h2 id="ce-que-ça-met-en-branle">Ce que ça met en branle</h2>

<p>C’est là que j’ai voulu regarder sous le capot. J’ai analysé les logs de session, les commits et les transcripts de sous-agents. Le chiffre qui m’a le plus surpris n’est pas le nombre de lignes de code, c’est le nombre d’agents.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>CallOfAcher (ASHFALL)</th>
      <th>FIFAcher</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fenêtre de travail</td>
      <td>26 → 31 juillet</td>
      <td>28 → 31 juillet</td>
    </tr>
    <tr>
      <td>Demandes qu’on a tapées</td>
      <td>45</td>
      <td>15</td>
    </tr>
    <tr>
      <td>Sous-agents lancés</td>
      <td>56</td>
      <td>36</td>
    </tr>
    <tr>
      <td>Tokens traités</td>
      <td>585 M</td>
      <td>562 M</td>
    </tr>
    <tr>
      <td>Tokens générés</td>
      <td>1,03 M</td>
      <td>1,15 M</td>
    </tr>
    <tr>
      <td>Logs de session</td>
      <td>465 Mo</td>
      <td>462 Mo</td>
    </tr>
    <tr>
      <td>Commits</td>
      <td>14</td>
      <td>12</td>
    </tr>
  </tbody>
</table>

<p>Au total : <strong>92 sous-agents</strong>, <strong>1,15 milliard de tokens traités</strong>, <strong>2,18 millions de tokens générés</strong>, pour <strong>60 demandes</strong> tapées par nous. Le rapport est vertigineux : une phrase en français déclenche des heures de travail machine. Trois modèles se sont relayés selon les phases (Opus 4.8, Opus 5, Fable 5).</p>

<p>Le “nous” est littéral : sur ces 60 demandes, deux seulement sont les prompts initiaux. Tout le reste a été formulé à deux, et une bonne partie tapée directement par mon neveu. Ces chiffres ne décrivent donc pas une performance de la machine seule, mais ce qu’une semaine de conversation à deux, dont un enfant de 10 ans, arrive à mettre en mouvement.</p>

<p>Le pattern d’orchestration mérite un mot. Pour FIFAcher, la construction initiale a été un workflow de 10 agents en parallèle, un par sous-système (foule, pelouse, ballon, éclairage, commentaire audio, post-processing…), chacun écrivant son module contre un contrat d’interface partagé : un fichier <code class="language-plaintext highlighter-rouge">CONTRACT.md</code> de 14 Ko généré en amont. Deux heures quarante de mur, dix agents concurrents. Puis des rondes de “critiques sévères” qui prennent des captures d’écran et notent le résultat, jusqu’à ce que les scores plafonnent.</p>

<p>Cette boucle de vérification visuelle est le cœur du dispositif, et elle est <em>laborieuse</em>. ASHFALL a laissé <strong>199 captures d’écran</strong> dans le dépôt, FIFAcher 31, plus 37 petits scripts de vérification (<code class="language-plaintext highlighter-rouge">voicecheck.mjs</code>, <code class="language-plaintext highlighter-rouge">switchcheck.mjs</code>, <code class="language-plaintext highlighter-rouge">rendercost.mjs</code>…) que l’agent a écrits pour s’auto-inspecter. Il ne <em>voit</em> pas le jeu tourner : il le photographie, image par image, et raisonne sur les photos.</p>

<h2 id="le-coût">Le coût</h2>

<p>Je l’ai reconstruit depuis les logs, aux tarifs API publics : <strong>entre 1 000 et 1 200 dollars</strong> pour les deux jeux (la fourchette dépend du TTL de cache appliqué). C’est une reconstruction, pas une facture, puisque j’étais sur un abonnement, mais l’ordre de grandeur est bon.</p>

<p>Le plus instructif n’est pas le montant, c’est <em>où</em> part l’argent. Certainement pas dans le code produit. Sur 1,15 milliard de tokens, le code généré ne pèse que <strong>2,18 millions de tokens, soit 0,19 % du volume</strong>. Les 99,8 % restants sont du <strong>contexte</strong> : à chaque appel, l’agent se refait expliquer le jeu qu’il est en train de modifier. Sur 5 295 appels API, cela fait en moyenne <strong>216 000 tokens de contexte par appel</strong>, relus encore et encore.</p>

<table>
  <thead>
    <tr>
      <th>Poste</th>
      <th>Volume</th>
      <th>Part du coût</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Lecture de contexte (cache)</td>
      <td>1,11 Md tokens</td>
      <td>55 %</td>
    </tr>
    <tr>
      <td>Écriture de contexte (cache)</td>
      <td>36,3 M tokens</td>
      <td>39 %</td>
    </tr>
    <tr>
      <td><strong>Code effectivement produit</strong></td>
      <td><strong>2,18 M tokens</strong></td>
      <td><strong>6 %</strong></td>
    </tr>
  </tbody>
</table>

<p>Autrement dit : <strong>on paie à 94 % le fait de lire et relire les milliers de lignes existantes, et à 6 % seulement le code écrit</strong>. C’est là que se joue le vrai coût, et c’est structurel. Plus le jeu grossit, plus la moindre évolution devient chère, y compris une évolution triviale. Ajouter une moto au cinquième jour coûte bien plus qu’ajouter la même moto au deuxième, non pas parce que la moto est plus compliquée, mais parce qu’il faut traverser tout l’existant pour la poser au bon endroit. On ne paie pas la fonctionnalité, on paie le contexte qu’elle oblige à parcourir. Un projet de 200 lignes se bricole pour quelques centimes ; le même geste sur 19 000 lignes coûte cent fois plus.</p>

<p>Et c’est le point douloureux. Ce n’est pas donné à tout le monde. Il faut aussi, a priori, des modèles frontier : je doute qu’un modèle deux crans en dessous tienne une boucle de 92 agents sans partir en vrille. Cela dit, je suis très confiant sur la démocratisation. Dans un an, ces 1 000 dollars en vaudront 50. Même aujourd’hui, on peut d’ores et déjà réduire drastiquement le coût en combinant des modèles plus légers et spécialisés avec des modèles frontier, ou en utilisant son expertise technique pour davantage cadrer le système agentique. Vu la répartition ci-dessus, c’est d’ailleurs sur le contexte qu’il faut agir en priorité : mieux découper les modules, mieux cibler ce qu’on donne à lire à l’agent, et ne pas laisser chaque sous-agent avaler tout le projet.</p>

<h2 id="tout-le-reste-sest-fait-à-deux">Tout le reste s’est fait à deux</h2>

<p>C’est la partie que je n’avais pas anticipée. Une fois le jeu créé, l’étendre devient une conversation. Mon neveu demande, je reformule à peine, il écrit même directement dans la console, et l’agent code. En une semaine, on a ajouté :</p>

<ul>
  <li><strong>tirer un ballon de foot</strong> sur des ennemis (avec, il faut le dire, un cri de célébration “SIUU” sur la touche V) ;</li>
  <li>des <strong>voitures et des motos</strong> conduisables ;</li>
  <li>plus d’ennemis, des civils qui fuient les coups de feu ;</li>
  <li>des <strong>décors réels</strong> : la Vallée de Gandy, le Stade Vélodrome, et le bourg de Saint-Aubin-du-Cormier avec son étang et les ruines de son château ;</li>
  <li>des <strong>réglages de difficulté</strong> ;</li>
  <li>le support <strong>manette</strong>, quasi immédiat, grâce à la Gamepad API du Web ;</li>
  <li>un <strong>mode mobile</strong> avec joysticks tactiles ;</li>
  <li>un <strong>mode coopératif en ligne</strong> à deux joueurs, en <strong>WebRTC pair-à-pair, sans serveur</strong> : un code à 4 caractères, et on joue ensemble.</li>
</ul>

<p>Le TL;DR : c’est <em>facile</em>. Un enfant de 10 ans y arrive et comprend très vite le principe. Oui, les temps d’attente sont longs, mais pendant ce temps on fait du vrai foot, des jeux de société, on se baigne, on vit, et on revient quand ça nous chante. Le cycle de développement épouse le rythme des vacances, ce qui est une phrase que je n’imaginais pas écrire un jour.</p>

<h2 id="la-stack-ou-pourquoi-le-web-change-tout">La stack, ou pourquoi le Web change tout</h2>

<p>Techniquement, c’est d’une simplicité désarmante, et c’est ça le point :</p>

<ul>
  <li><strong>Three.js</strong> (r185) et <strong>rien d’autre</strong> côté rendu. Aucun asset externe : toutes les textures, tous les modèles, tous les sons sont <strong>générés en code</strong>. La foule du stade de FIFAcher, ce sont des dizaines de milliers de supporters dans un seul <code class="language-plaintext highlighter-rouge">InstancedMesh</code>, donc un seul appel de rendu, avec un atlas de 64 cellules peint procéduralement sur un canvas, et un masque séparé pour ne teinter que les vêtements et jamais la peau.</li>
  <li>~18 900 lignes de JavaScript pour ASHFALL, réparties en 19 modules ES.</li>
  <li><strong>Vite</strong> pour le build, <strong>Playwright/Puppeteer</strong> pour les captures automatiques.</li>
  <li><strong>PeerJS/WebRTC</strong> pour le co-op, le broker gratuit servant <em>uniquement</em> à la signalisation.</li>
  <li>Déploiement <strong>statique sur GitHub Pages</strong> via GitHub Actions. Zéro serveur, zéro infra, zéro coût d’hébergement.</li>
</ul>

<p>C’est ce dernier point qui rend l’affaire réelle plutôt qu’anecdotique. Un <code class="language-plaintext highlighter-rouge">git push</code>, et le jeu est en ligne, pour n’importe qui, partout, immédiatement. Pas de store, pas de validation, pas de build par plateforme. Le même fichier <code class="language-plaintext highlighter-rouge">index.html</code> sert le gamin sur son téléphone dans le train et l’adulte avec sa manette devant son écran. Allez cliquer sur les deux liens du début, c’est plus rapide que de finir ce paragraphe.</p>

<h2 id="ce-qui-ne-marche-pas">Ce qui ne marche pas</h2>

<p>Soyons nets : <strong>ce n’est pas le niveau d’un vrai Call of Duty ni d’un vrai FIFA</strong>. L’écart est réel, et il a plusieurs causes.</p>

<p><strong>Le graphisme et la 3D.</strong> C’est là que le fossé est le plus visible. Du procédural sans artiste, ça se voit.</p>

<p><strong>Les instructions parfois ignorées.</strong> Il arrive qu’une fonctionnalité demandée ne soit tout simplement pas implémentée, sans que rien ne le signale. Il faut re-demander.</p>

<p><strong>La vérification reste de surface.</strong> Les critiques-agents notent des captures d’écran, ils ne jouent pas. Résultat : des bugs et des glitchs un peu partout, y compris des choses qu’un humain repère en trois secondes de manette.</p>

<p><strong>La jouabilité et le tuning : dur, dur.</strong> C’est le point le plus difficile. Régler la sensibilité, l’équilibrage, le “feel”, ça demande des dizaines d’allers-retours, et c’est le domaine où l’agent est le plus démuni.</p>

<p><strong>La connaissance des décors est réelle mais mal restituée.</strong> Sur le Vélodrome, le modèle <em>sait</em> des choses justes : la forme des virages, l’ambiance. Mais la restitution reste approximative. Sur d’autres décors, malgré une connaissance manifeste, le résultat est franchement bullshit. Il faut travailler bien plus.</p>

<p>Cela dit, une nuance amusante : pour FIFAcher, la faiblesse technique <strong>devient presque une feature</strong>. Le jeu ne se joue pas comme un FIFA, il force à trouver d’autres stratégies. Le gameplay est différent, pas seulement inférieur.</p>

<h2 id="le-vrai-plafond">Le vrai plafond</h2>

<p><a href="https://x.com/karpathy/status/2083749667410727319">Karpathy a posté</a> une réflexion qui recoupe exactement ce que j’ai observé. Deux idées m’ont marqué.</p>

<p>La première : on quitte le territoire du “dessine-moi un pélican en SVG”. Ces jeux relèvent d’une catégorie de tâches que <strong>personne de sensé n’aurait entreprises</strong>, trop custom, trop longues, aucun retour sur investissement. Les LLMs ont toute la patience du monde, donc on passe de “personne ne ferait jamais ça” à “pourquoi pas, c’est quasi gratuit”. Des mondes hyper-personnalisés à la demande. C’est exactement notre expérience : Saint-Aubin-du-Cormier en jeu vidéo, ça n’existait pas et ça n’aurait jamais existé.</p>

<p>La seconde, plus sévère, explique mes bugs : <strong>les modèles ne savent pas auditer leur propre travail</strong> dans ce domaine, parce qu’ils ne perçoivent pas nativement la vidéo et ne jouent pas au jeu. Mes 199 captures d’écran sont la preuve matérielle de ce handicap : une boucle de vérification lente et douloureuse, là où un humain avec une manette tranche en cinq secondes.</p>

<p>Ce n’est pas qu’une impression. On a mesuré le phénomène dans un <a href="https://arxiv.org/abs/2606.15693">papier récent sur la vérification visuelle imparfaite</a>, en prenant TikZ comme cas d’étude : jusqu’où le raffinement itératif reste-t-il efficace quand le vérificateur lui-même n’est pas fiable ? Les vérificateurs visuels, LLM ou outillés, ne dépassent pas un <strong>F1 de 0,815</strong> pour déterminer si une instruction visuelle a bien été appliquée au code. Autrement dit : dans un cas sur cinq environ, la boucle “génère, regarde, corrige” se trompe sur son propre diagnostic. On observe aussi que le feedback n’aide vraiment que s’il identifie précisément le défaut de l’image et propose une correction actionnable, sinon il ajoute du bruit. Et si TikZ, qui produit des figures statiques en 2D, pose déjà ce problème, on imagine l’ampleur du gap sur un monde 3D animé qu’il faudrait <em>jouer</em> pour évaluer.</p>

<p>Tant que ce verrou tient, l’écart avec un vrai AAA restera.</p>

<h2 id="le-wow-et-les-problèmes-ouverts">Le “wow”, et les problèmes ouverts</h2>

<p><strong>Chacun peut créer son jeu.</strong> C’est ça, l’incroyable. Le nom des joueurs, les équipes, le stade, le décor : tout est personnalisable. Dans FIFAcher, l’équipe locale est composée de onze Acher, prénoms sur les maillots, chacun avec sa couleur de cheveux. Fini le temps où les add-ons étaient réservés aux hackers.</p>

<p><strong>Les droits d’auteur.</strong> J’ai demandé d’incorporer du son sous licence (Jump pour l’entrée des joueurs) : refus net, et à raison. Mais la frontière est floue. Qu’est-ce qui m’interdit de mettre les vrais noms de joueurs ? Le modèle, lui, a tracé sa propre ligne sans qu’on le lui demande : il a dessiné des blasons <em>originaux</em> aux couleurs des clubs, en écrivant explicitement dans le README que les vrais écussons ne sont pas reproduits. Prudence apprise, pas règle explicite.</p>

<p><strong>Le multimodal ouvre la suite.</strong> Plein de composants open source existent pour la modélisation 3D. Rien n’empêche de bâtir de vrais petits ateliers pour modéliser sérieusement les décors, les joueurs, les stades, avec l’IA évidemment. C’est le chaînon manquant du graphisme.</p>

<p><strong>Un FIFA “open” peut-il émerger ?</strong> Techniquement, la brique est là. Mais je n’en suis pas si sûr, et pour une raison sociale, pas technique : une <em>plateforme</em> partagée peut-elle émerger quand n’importe qui peut partir avec son propre variant ? La personnalisation à l’extrême est peut-être l’ennemie de la masse critique.</p>

<p><strong>La frontière IA / dev de jeu.</strong> On a développé dans la console. Si si. Et clairement, on avait envie d’éditer le jeu <em>dans</em> le jeu. Le prochain saut d’ergonomie est peut-être là.</p>

<p><strong>On n’a jamais regardé le code.</strong> Jamais parlé du code, non plus, et tant mieux : c’est ce qui a rendu la collaboration avec un enfant de 10 ans possible. Mais il faut de l’expérience pour recadrer le modèle quand ça dérape, pour déployer, pour comprendre pourquoi ça rame. Pas beaucoup plus, à ce niveau de complexité. Le fameux fossé arrivera plus loin : quand la complexité et l’exigence monteront (un vrai AAA), il faudra se réapproprier certains éléments techniques.</p>

<p>Reste le plus simple, et c’est peut-être le vrai argument. Les deux jeux sont là, à une URL, gratuits, sans installation : <strong><a href="https://blog.mathieuacher.com/ashfall/">ashfall</a></strong> et <strong><a href="https://blog.mathieuacher.com/fifacher/">fifacher</a></strong>. Prenez une manette, ou votre téléphone, jouez cinq minutes. Vous verrez les coutures, elles sont nombreuses. Mais vous jouerez à quelque chose que personne n’aurait jamais pris le temps de fabriquer, écrit en une semaine de vacances par un chercheur et un gamin de 10 ans qui n’a pas lu une ligne de code. C’est ça, la nouvelle. Et ça n’a pas fini de bouger.</p>

<hr />

<p><em>Les deux jeux : <a href="https://blog.mathieuacher.com/ashfall/">ashfall</a> (<a href="https://github.com/acherm/ashfall">code</a>, <a href="https://www.youtube.com/shorts/DUihik38JEA">démo vidéo</a>) et <a href="https://blog.mathieuacher.com/fifacher/">fifacher</a> (<a href="https://github.com/acherm/fifacher">code</a>, <a href="https://www.youtube.com/shorts/CMiOrjE6TMY">démo vidéo</a>). Prompt initial de <a href="https://x.com/mattshumer_/status/2081100590612922527">Matt Shumer</a> (<a href="https://github.com/mshumer/Claude-of-Duty/blob/main/prompt.md">dépôt</a>), adapté pour le football.</em></p>

<p><em>Méthodologie des chiffres : reconstruction depuis les transcripts de session Claude Code (927 Mo de JSONL). Les requêtes sont dédupliquées par identifiant de message, et les sessions reprises (qui rejouent l’historique de la session précédente) sont fusionnées pour ne pas compter deux fois. Les “demandes” comptent uniquement ce qu’on a réellement écrit : les notifications automatiques de fin de tâche, les échos de commandes et les “continue” sont exclus. Le coût est estimé aux tarifs API publics (input, output, écriture et lecture de cache), pas relevé sur une facture. Modèles : Claude Opus 4.8, Opus 5 et Fable 5.</em></p>

<p><em>Sur la vérification visuelle : Charly Reux, Mathieu Acher, Djamel Eddine Khelladi, Clément Quinton, Olivier Barais, <a href="https://arxiv.org/abs/2606.15693">Imperfect Visual Verification for Code Edition: A Case Study on TikZ</a>, 2026.</em></p>

<p><em>Un avis, une idée, un bug trouvé ? mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026callofacher</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{CallOfAcher et FIFAcher : vibe coder des variantes de jeux avec mon neveu de 10 ans}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{aug}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CallOfAcherFIFAcherJeuxIA/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="LLM" /><category term="coding agents" /><category term="Claude Code" /><category term="Three.js" /><category term="jeux vidéo" /><category term="generative AI" /><category term="llm4code" /><category term="WebRTC" /><category term="GitHub Pages" /><category term="sous-agents" /><summary type="html"><![CDATA[Il y a une semaine, j’ai collé ce prompt de Matt Shumer (150 mots, pas une ligne de spec technique) dans Claude Code, et je suis allé faire autre chose. En rentrant : un FPS jouable dans le navigateur. Pas une démo, pas un prototype. Un jeu. J’ai recommencé deux jours plus tard en remplaçant “Call of Duty” par “FIFA”, et ça a marché aussi. Puis mon neveu de 10 ans a fait évoluer les deux jeux, en donnant des idées, en écrivant directement ce qu’il voulait dans le terminal de Claude, en jouant, en critiquant, et ainsi de suite, jusqu’à aujourd’hui. Les deux jeux sont en ligne, jouables tout de suite (même sur téléphone !), sans installer quoi que ce soit : blog.mathieuacher.com/ashfall : le FPS, avec son mode coopératif à deux, ses décors, etc. (démo vidéo) blog.mathieuacher.com/fifacher : le foot, un match au Stade Raoul Brulat avec les cigales et deux joueurs d’exception. (démo vidéo)]]></summary></entry><entry><title type="html">Le test du lave-auto : les LLMs ont-ils vraiment du sens commun ?</title><link href="http://blog.mathieuacher.com/LLMCarWashCommonSense/" rel="alternate" type="text/html" title="Le test du lave-auto : les LLMs ont-ils vraiment du sens commun ?" /><published>2026-05-26T06:00:00+00:00</published><updated>2026-05-26T06:00:00+00:00</updated><id>http://blog.mathieuacher.com/LLMCarWashCommonSense</id><content type="html" xml:base="http://blog.mathieuacher.com/LLMCarWashCommonSense/"><![CDATA[<p>L’exemple est devenu un classique : <em>“Je dois laver ma voiture. Le centre de lavage est à 100 m. J’y vais à pied ou en voiture ?”</em>. Réponse de sens commun : en voiture, évidemment, sinon la voiture n’est pas là pour être lavée. Sauf que la plupart des LLMs tombent dans le panneau et recommandent la marche pour des raisons d’écologie ou de courte distance.</p>

<p><a href="https://x.com/Fabien_Mikol/status/2058516722139701340">Fabien Mikol a relayé/relancé l’affaire il y a quelques jours, suite aux propos de Yann Lecun</a>, <a href="https://x.com/GrablyR/status/2058548560275058971">Raphael Grably a vérifié dans la foulée, avec d’autres</a>. La question circule régulièrement comme test informel des capacités de raisonnement des LLMs. J’avais déjà fait un mini-test en février 2025 (72 réponses, 71 échecs) puis j’étais passé à autre chose. Entre temps, GPT-5, Claude 4.7, Gemini 2.5 Pro, Qwen3-thinking et compagnie sont sortis. Donc je me suis dit : refaisons l’expérience sérieusement. Plus de modèles, plus de répétitions, plusieurs formulations, un juge LLM pour scorer.</p>

<p>Voici ce que j’ai trouvé sur plus de 4 300 réponses LLM générées et près de 4 000 jugements, plus un échantillon de 128 items re-jugés en parallèle par 4 agents Claude indépendants pour valider la méthodologie. Chaque réponse est notée sur deux critères évalués séparément : (a) la conclusion finale recommande-t-elle la voiture ? et (b) la réponse évite-t-elle de justifier sa recommandation par la courte distance ? Quatre surprises, dans l’ordre où elles me sont tombées dessus :</p>

<ol>
  <li>Le piège reste largement actif. Quinze mois après mon premier test, avec des modèles plus gros, plus chers, censément “raisonneurs”, la majorité des LLMs se font encore avoir. Quatre modèles sur huit n’atteignent jamais 25 % de bonne conclusion.</li>
  <li>Reformuler la question peut faire basculer un modèle de plus de 60 points de pourcentage, dans un sens ou dans l’autre. Effet stable à n=120 répétitions, pas du bruit. Cette fragilité concerne le mode d’inférence par défaut : activer un effort de raisonnement explicite la rattrape pour certains modèles (Claude Sonnet), mais pas pour d’autres (DeepSeek R1, qui pourtant raisonne par construction).</li>
  <li>GPT-5.5 sort du lot, mais pas où on l’attendrait. Sur la capacité à conclure correctement, plusieurs modèles le rejoignent quand on active leur raisonnement (Sonnet 4.6, Qwen3-thinking, à 95-100 %). Sa singularité est ailleurs : il est le seul à éviter systématiquement d’invoquer la “courte distance” comme excuse, là où aucun autre modèle ne dépasse 58 % sur ce critère, même à effort maximal.</li>
  <li>Le choix de la métrique change le classement. Claude Opus 4.7 paraissait dans le mou (~20 % de succès) avec un critère composite “conclusion correcte ET pas de mention de distance”. Sur la conclusion seule, il est à 97 % : il a la bonne intuition presque toujours, il discute juste systématiquement la distance avant d’y arriver. Une bonne partie du classement initial reflétait la verbosité, pas la capacité.</li>
</ol>

<p><em>Code et données : <a href="https://github.com/acherm/laveauto-llm">laveauto-llm</a>. Tous les modèles appelés via <a href="https://openrouter.ai">OpenRouter</a>. Juge LLM = GPT-5.5 à température 0, deux verdicts indépendants par réponse (C1 = recommande la voiture, C2 = ne justifie pas par la distance).</em></p>

<hr />

<h2 id="1-le-dispositif">1. Le dispositif</h2>

<p>J’ai conduit deux expériences en parallèle, mêmes conditions (mêmes modèles, mêmes répétitions, même juge), mais deux formulations différentes du piège :</p>

<ul>
  <li>V1 (la formulation “originale”, celle de février 2025) : <em>“Je veux laver ma voiture. Le lave auto est à 150 mètres de chez moi. J’y vais à pied ou en voiture ?”</em></li>
  <li>V2 (une reformulation en oui/non, <a href="https://x.com/GrablyR/status/2058548560275058971">utilisée notamment par GrablyR</a>) : <em>“Je dois aller laver ma voiture. Le centre de lavage automobile est à 100 mètres de chez moi. Dois-je y aller à pieds ?”</em></li>
</ul>

<p>L’intérêt d’en faire deux : tester ce qui est censé être la même question avec deux mises en forme proches. On verra dans les sections 4 et 5 que ce choix s’est avéré central. Pour l’instant, gardons en tête que tout ce qui suit existe en double : un score V1 et un score V2 par modèle.</p>

<p>Bonne réponse attendue dans les deux cas : <em>“en voiture”</em>, sinon la voiture n’est pas au lave-auto.</p>

<p>Critères du juge, évalués indépendamment :</p>
<ul>
  <li>C1 : la conclusion finale recommande la voiture.</li>
  <li>C2 : la réponse ne justifie pas une recommandation par la courte distance (un argument écologique séduisant mais hors-sujet, qui détourne du piège réel).</li>
</ul>

<p>Toutes les tables qui suivent reportent les deux critères séparément. La colonne “strict” correspond à C1 et C2 vrais tous les deux. La dissociation a été cruciale : un premier juge global réussi/raté pénalisait à tort des modèles qui avaient la bonne conclusion mais discutaient la distance en chemin.</p>

<p>Conditions principales : 8 modèles, n = 120 par modèle et par variante (240 réponses par modèle au total), température 0.7, aucun system prompt. Juge = GPT-5.5 à température 0.</p>

<p>8 modèles testés (mai 2026) :</p>
<ul>
  <li>Frontière fermée : <code class="language-plaintext highlighter-rouge">openai/gpt-5.5</code>, <code class="language-plaintext highlighter-rouge">anthropic/claude-opus-4.7</code>, <code class="language-plaintext highlighter-rouge">anthropic/claude-sonnet-4.6</code>, <code class="language-plaintext highlighter-rouge">openai/gpt-4o</code> (baseline), <code class="language-plaintext highlighter-rouge">google/gemini-2.5-pro</code></li>
  <li>Poids ouverts : <code class="language-plaintext highlighter-rouge">meta-llama/llama-3.3-70b-instruct</code>, <code class="language-plaintext highlighter-rouge">mistralai/mistral-large-2512</code>, <code class="language-plaintext highlighter-rouge">deepseek/deepseek-chat-v3.1</code></li>
</ul>

<hr />

<h2 id="2-le-verdict-de-base--conclusion-correcte-vs-critère-strict">2. Le verdict de base : conclusion correcte vs critère strict</h2>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>C1 : conclusion = voiture</th>
      <th>C2 : pas de justif distance</th>
      <th>Strict (C1 + C2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>openai/gpt-5.5</td>
      <td><strong>100.0 %</strong></td>
      <td><strong>64.2 %</strong></td>
      <td><strong>64.2 %</strong></td>
    </tr>
    <tr>
      <td>anthropic/claude-opus-4.7</td>
      <td><strong>97.5 %</strong></td>
      <td>5.8 %</td>
      <td>5.8 %</td>
    </tr>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td><strong>96.7 %</strong></td>
      <td>26.7 %</td>
      <td>26.7 %</td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td>38.1 %</td>
      <td>4.2 %</td>
      <td>4.2 %</td>
    </tr>
    <tr>
      <td>meta-llama/llama-3.3-70b-instruct</td>
      <td>23.3 %</td>
      <td>20.8 %</td>
      <td>15.0 %</td>
    </tr>
    <tr>
      <td>deepseek/deepseek-chat-v3.1</td>
      <td>5.8 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>mistralai/mistral-large-2512</td>
      <td>4.2 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>openai/gpt-4o</td>
      <td>0.8 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
  </tbody>
</table>

<p>(IC95 Wilson, n=120 par modèle, voir <code class="language-plaintext highlighter-rouge">results_rejudge_v1.json</code> pour le détail.)</p>

<p>Selon le critère utilisé, deux histoires. Sur la conclusion seule (C1), trois modèles dépassent 96 % (GPT-5.5, Opus 4.7, Sonnet 4.6) et quatre restent sous 25 %. Partage net entre “comprend le piège” et “n’a pas compris”. Sur le strict, seul GPT-5.5 dépasse 50 % : le critère “ne pas justifier par la distance” est en pratique très dur, même pour les modèles qui concluent juste. Opus 4.7 et Sonnet 4.6 mentionnent presque toujours la distance, et un benchmark à critères agrégés les sous-estime.</p>

<p>Trois observations qualitatives :</p>

<ul>
  <li>GPT-4o n’obtient quasiment aucun bon verdict (1 sur 120 en C1, 0 en strict). La réponse est souvent quasi-identique : <em>“Si le lave-auto est à seulement 150 mètres, il est plus écologique et pratique d’y aller à pied.”</em>. À température 0.7 on s’attendrait à de la variation, et il y en a un peu sur la formulation, mais le squelette et la conclusion sont stables (vérifié aussi à temp 0.3 et 1.0). Le dernier snapshot public est <code class="language-plaintext highlighter-rouge">gpt-4o-2024-11-20</code>, avec une <a href="https://help.openai.com/en/articles/9624314-model-release-notes">mise à jour le 29 janvier 2025</a> (cutoff de connaissance étendu à juin 2024). Le modèle a été <a href="https://help.openai.com/articles/20001051">retiré de ChatGPT le 13 février 2026</a>, mais reste disponible via l’API où il continue d’alimenter de nombreux produits. Ce qu’on observe ici, c’est donc le comportement d’un modèle figé sur ce piège — et figé en restant largement utilisé en aval.</li>
  <li>Opus 4.7 et Sonnet 4.6 sont au coude-à-coude sur C1 (97-98 %), avec des styles différents. Opus commence souvent par <em>“À pied !”</em> puis se ravise vers la voiture en fin de réponse ; Sonnet structure mieux son argumentaire. Le mythe “Opus &gt; Sonnet” ne se vérifie pas ici, mais pas non plus l’inverse.</li>
  <li>Gemini 2.5 Pro est bimodal sur C1 : 38 % de bonne conclusion, le reste à pied. Sur les ~75 réponses fausses, c’est la même structure “Excellente question existentielle…” suivie de “à pied, c’est plus écologique”. Llama 3.3 montre la même bimodalité.</li>
</ul>

<hr />

<h2 id="3-le-raisonnement-aide-sauf-quand-il-renforce-le-mauvais-prior">3. Le raisonnement aide, sauf quand il renforce le mauvais prior</h2>

<p>Pour les modèles qui supportent un effort de raisonnement explicite (<code class="language-plaintext highlighter-rouge">reasoning.effort=high</code> via OpenRouter), j’ai relancé V1 et V2 sur 4 modèles, n=100 par cellule. Les non-éligibles (GPT-4o, Mistral Large, Llama 3.3, DeepSeek-chat v3.1) ne sont pas reasoning-capables ; GPT-5.5 est déjà à 100 %. 800 réponses générées, jugement dissocié C1/C2.</p>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>Variante</th>
      <th>C1</th>
      <th>C2</th>
      <th>Strict</th>
      <th>Tokens raison. (moy.)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td>V1</td>
      <td><strong>100 %</strong></td>
      <td>58 %</td>
      <td>58 %</td>
      <td>167</td>
    </tr>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td>V2</td>
      <td><strong>99 %</strong></td>
      <td>49 %</td>
      <td>49 %</td>
      <td>183</td>
    </tr>
    <tr>
      <td>qwen/qwen3-235b-a22b-thinking</td>
      <td>V1</td>
      <td>95 %</td>
      <td>29 %</td>
      <td>29 %</td>
      <td>1 967</td>
    </tr>
    <tr>
      <td>qwen/qwen3-235b-a22b-thinking</td>
      <td>V2</td>
      <td>96 %</td>
      <td>6 %</td>
      <td>6 %</td>
      <td>1 498</td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td>V1</td>
      <td>58 %</td>
      <td>11 %</td>
      <td>11 %</td>
      <td>1 427</td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td>V2</td>
      <td>95 %</td>
      <td>57 %</td>
      <td>57 %</td>
      <td>1 224</td>
    </tr>
    <tr>
      <td>deepseek/deepseek-r1</td>
      <td>V1</td>
      <td>7 %</td>
      <td>1 %</td>
      <td>1 %</td>
      <td>455</td>
    </tr>
    <tr>
      <td>deepseek/deepseek-r1</td>
      <td>V2</td>
      <td>14 %</td>
      <td>2 %</td>
      <td>2 %</td>
      <td>467</td>
    </tr>
  </tbody>
</table>

<h3 id="sans-raisonnement-vs-avec-raisonnement-high-c1-uniquement">Sans raisonnement vs avec raisonnement HIGH (C1 uniquement)</h3>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>V1 sans</th>
      <th>V1 avec</th>
      <th>Δ</th>
      <th>V2 sans</th>
      <th>V2 avec</th>
      <th>Δ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td>97 %</td>
      <td>100 %</td>
      <td>+3</td>
      <td>39 %</td>
      <td><strong>99 %</strong></td>
      <td><strong>+60</strong></td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td>38 %</td>
      <td>58 %</td>
      <td>+20</td>
      <td>96 %</td>
      <td>95 %</td>
      <td>−1</td>
    </tr>
  </tbody>
</table>

<p>Le résultat le plus marquant : <strong>activer le raisonnement sur Sonnet 4.6 fait disparaître presque entièrement sa fragilité sur V2</strong> (39 % → 99 %). C’est le seul cas du dataset où une intervention transforme un modèle fragile en modèle stable à la reformulation.</p>

<p>Observations sur les deux autres modèles testés :</p>

<ul>
  <li>DeepSeek R1, modèle reasoning par construction, reste planté à 7-14 % C1 dans les deux variantes. Augmenter l’effort à <code class="language-plaintext highlighter-rouge">high</code> (vs <code class="language-plaintext highlighter-rouge">medium</code>, déjà testé séparément) ne change rien. Les ~450 tokens de raisonnement sont consommés à argumenter en boucle pour la marche. Le prior tient, le raisonnement sert à l’étayer.</li>
  <li>Qwen3-thinking est très stable sur la conclusion (95-96 % C1 dans les deux variantes), avec ~1 500-2 000 tokens de raisonnement par appel. Sur le strict, il reste très pénalisé : il discute presque toujours la distance, encore plus en V2.</li>
</ul>

<p>Note méthodologique : dans une exploration préliminaire à n=10 avec l’ancien juge global, Qwen3-thinking marquait “100 %” sur V1. Avec dissocié et n=100, c’est 95 % C1 et 29 % strict. Le “100 %” était en partie un artefact du juge initial sur petit échantillon.</p>

<p>Sur OpenRouter, <code class="language-plaintext highlighter-rouge">qwen/qwen3-235b-a22b-thinking-2507</code> refuse même de répondre avec <code class="language-plaintext highlighter-rouge">reasoning.enabled=false</code> (erreur 400 : <em>“Reasoning is mandatory for this endpoint and cannot be disabled.”</em>). On ne peut donc pas isoler “même modèle, sans thinking”.</p>

<h3 id="et-chez-openai--la-lignée-gpt-5-à-effort-variable">Et chez OpenAI ? La lignée GPT-5 à effort variable</h3>

<p>Pour compléter, j’ai testé deux ancêtres de GPT-5.5 — <code class="language-plaintext highlighter-rouge">gpt-5</code> (août 2025) et <code class="language-plaintext highlighter-rouge">gpt-5.1</code> (fin 2025) — aux trois efforts disponibles (low, medium, high), V1 et V2, n=100 par cellule.</p>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>Effort</th>
      <th>V1 C1</th>
      <th>V1 C2</th>
      <th>V2 C1</th>
      <th>V2 C2</th>
      <th>Tok. raison. (V1)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>gpt-5</td>
      <td>low</td>
      <td>100 %</td>
      <td>44 %</td>
      <td>99 %</td>
      <td>31 %</td>
      <td>272</td>
    </tr>
    <tr>
      <td>gpt-5</td>
      <td>medium</td>
      <td>100 %</td>
      <td>23 %</td>
      <td>99 %</td>
      <td>45 %</td>
      <td>709</td>
    </tr>
    <tr>
      <td>gpt-5</td>
      <td>high</td>
      <td>100 %</td>
      <td>27 %</td>
      <td>100 %</td>
      <td>42 %</td>
      <td>1 341</td>
    </tr>
    <tr>
      <td>gpt-5.1</td>
      <td><strong>low</strong></td>
      <td>75 %</td>
      <td>24 %</td>
      <td><strong>13 %</strong></td>
      <td>1 %</td>
      <td><strong>7</strong></td>
    </tr>
    <tr>
      <td>gpt-5.1</td>
      <td>medium</td>
      <td>98 %</td>
      <td>35 %</td>
      <td>79 %</td>
      <td>31 %</td>
      <td>108</td>
    </tr>
    <tr>
      <td>gpt-5.1</td>
      <td>high</td>
      <td>100 %</td>
      <td>51 %</td>
      <td>93 %</td>
      <td>46 %</td>
      <td>210</td>
    </tr>
  </tbody>
</table>

<p>Quatre enseignements :</p>

<ul>
  <li>À effort <code class="language-plaintext highlighter-rouge">low</code>, <strong>gpt-5.1 est dramatiquement pire que gpt-5</strong> (V2 : 13 % vs 99 % C1). La régression apparente s’explique par la consommation : gpt-5 à <code class="language-plaintext highlighter-rouge">low</code> alloue encore ~272 tokens de raisonnement, gpt-5.1 à <code class="language-plaintext highlighter-rouge">low</code> n’en alloue que ~7. OpenAI a vraisemblablement redéfini ce que <code class="language-plaintext highlighter-rouge">low</code> signifie, pour en faire un mode quasi non-reasoner. Conséquence pratique : un utilisateur qui passe à gpt-5.1 en gardant <code class="language-plaintext highlighter-rouge">effort=low</code> peut voir ses résultats s’effondrer sans changer son code. Comparer un même <code class="language-plaintext highlighter-rouge">effort</code> entre deux versions n’est pas comparer un même budget de calcul.</li>
  <li>Lecture côté produit grand public : autour de février 2026, ChatGPT servait gpt-5.1 en mode “instant” (sans étape de réflexion explicite, équivalent à effort <code class="language-plaintext highlighter-rouge">low</code>) pour les réponses rapides. Sur ce piège, ça correspond donc à 75 % de bonne conclusion sur la formulation V1, et <strong>seulement 13 % sur V2</strong> — soit ce que recevaient les utilisateurs “instant” de ChatGPT à ce moment-là. Aujourd’hui (mai 2026), ChatGPT Free <a href="https://help.openai.com/en/articles/9275245-using-chatgpt-s-free-tier-faq">tourne sur GPT-5.5</a> (avec limites d’usage), qui passe le piège à 100 %. Mais entre les produits tiers qui consomment l’API et les chatbots qui n’utilisent pas la dernière génération, la version “13 % sur une simple reformulation” est probablement encore largement servie.</li>
  <li>Le seuil “minimum de calcul pour résister à la reformulation” est très visible dans la lignée gpt-5.1 : 7 tokens (low) = 13 % V2, ~108 tokens (medium) = 79 %, ~210 tokens (high) = 93 %. Il existe une bande étroite en dessous de laquelle la robustesse s’effondre, et au-dessus de laquelle elle tient.</li>
  <li>gpt-5 (août 2025), même à <code class="language-plaintext highlighter-rouge">low</code>, atteint 99-100 % C1 dans les deux variantes. La robustesse à la reformulation sur la conclusion existe dans la lignée OpenAI depuis le début de la famille GPT-5.</li>
  <li>Ce qui évolue monotonement entre gpt-5, gpt-5.1 et gpt-5.5 à effort élevé, c’est le C2 : 23-44 % chez gpt-5, 24-51 % chez gpt-5.1, 64-72 % chez gpt-5.5. C’est sur la parcimonie du raisonnement (ne pas convoquer la distance) que la lignée s’affine, plus que sur la conclusion elle-même.</li>
</ul>

<p>(Un caveat méthodologique sur la lecture des <code class="language-plaintext highlighter-rouge">reasoning_tokens</code> arrive en section 6.)</p>

<hr />

<h2 id="4-changer-la-formulation--tout-seffondre-ou-explose">4. Changer la formulation : tout s’effondre (ou explose)</h2>

<p>On passe maintenant à la deuxième formulation annoncée en section 1 :</p>

<blockquote>
  <p><em>“Je dois aller laver ma voiture. Le centre de lavage automobile est à 100 mètres de chez moi.</em></p>

  <p><em>Dois-je y aller à pieds?”</em></p>
</blockquote>

<p>Question oui/non, distance 100 m, vocabulaire “centre de lavage automobile” au lieu de “lave auto”. Tout le reste est inchangé : mêmes 8 modèles, n = 120, température 0.7, pas de system prompt, mêmes critères dissociés.</p>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>C1 : conclusion = voiture</th>
      <th>C2 : pas de justif distance</th>
      <th>Strict (C1 + C2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>openai/gpt-5.5</td>
      <td><strong>100.0 %</strong></td>
      <td><strong>71.7 %</strong></td>
      <td><strong>71.7 %</strong></td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td><strong>95.8 %</strong></td>
      <td>40.0 %</td>
      <td>40.0 %</td>
    </tr>
    <tr>
      <td>anthropic/claude-opus-4.7</td>
      <td><strong>95.0 %</strong></td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td>39.2 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>deepseek/deepseek-chat-v3.1</td>
      <td>15.0 %</td>
      <td>1.7 %</td>
      <td>1.7 %</td>
    </tr>
    <tr>
      <td>openai/gpt-4o</td>
      <td>1.7 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>meta-llama/llama-3.3-70b-instruct</td>
      <td>0 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
    <tr>
      <td>mistralai/mistral-large-2512</td>
      <td>0 %</td>
      <td>0 %</td>
      <td>0 %</td>
    </tr>
  </tbody>
</table>

<p>Comparons côte-à-côte le C1 entre V1 et V2 :</p>

<table>
  <thead>
    <tr>
      <th>Modèle</th>
      <th>V1 C1</th>
      <th>V2 C1</th>
      <th>Δ</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>openai/gpt-5.5</td>
      <td>100.0 %</td>
      <td>100.0 %</td>
      <td>0</td>
    </tr>
    <tr>
      <td>anthropic/claude-opus-4.7</td>
      <td>97.5 %</td>
      <td>95.0 %</td>
      <td>−2.5</td>
    </tr>
    <tr>
      <td>anthropic/claude-sonnet-4.6</td>
      <td>96.7 %</td>
      <td>39.2 %</td>
      <td><strong>−57.5</strong></td>
    </tr>
    <tr>
      <td>google/gemini-2.5-pro</td>
      <td>38.1 %</td>
      <td>95.8 %</td>
      <td><strong>+57.7</strong></td>
    </tr>
    <tr>
      <td>meta-llama/llama-3.3-70b-instruct</td>
      <td>23.3 %</td>
      <td>0 %</td>
      <td>−23.3</td>
    </tr>
    <tr>
      <td>deepseek/deepseek-chat-v3.1</td>
      <td>5.8 %</td>
      <td>15.0 %</td>
      <td>+9.2</td>
    </tr>
    <tr>
      <td>mistralai/mistral-large-2512</td>
      <td>4.2 %</td>
      <td>0 %</td>
      <td>−4.2</td>
    </tr>
    <tr>
      <td>openai/gpt-4o</td>
      <td>0.8 %</td>
      <td>1.7 %</td>
      <td>+0.9</td>
    </tr>
  </tbody>
</table>

<p>Sonnet 4.6 et Gemini 2.5 Pro basculent de presque 58 points en direction opposée, même en isolant la conclusion. Cela enterre l’idée qu’on peut classer la capacité au sens commun d’un modèle avec un seul prompt.</p>

<p>Pourquoi ? Disclaimer immédiat : tout ce qui suit relève de l’interprétation, pas de la démonstration. Mes données me disent <em>que</em> les modèles basculent, pas <em>pourquoi</em>, et je n’ai pas conduit les expériences d’ablation qui permettraient de l’établir. Sous cette réserve : pour Sonnet, la forme oui/non de V2 semble corrélée à un comportement d’acquiescement (réponses qui commencent par <em>“Oui, sans hésiter, 100 mètres c’est très court”</em>). Effet de la syntaxe interrogative, du vocabulaire (“centre de lavage automobile” plutôt que “lave auto”), de la longueur du prompt, ou combinaison des trois — je ne peux pas trancher. Pour Gemini, la même formulation coïncide avec une rumination plus longue (1 525 tokens en sortie) au bout de laquelle il arrive à <em>“Non”</em>. Verbosité associée au succès, peut-être pas causale.</p>

<p>GPT-5.5 et Opus 4.7 sont les seuls stables sur la conclusion (95+ % sur les deux formulations). À l’autre bout, GPT-4o, Mistral et Llama échouent quel que soit le cadrage.</p>

<hr />

<h2 id="5-le-vrai-problème--la-sensibilité-au-prompt">5. Le vrai problème : la sensibilité au prompt</h2>

<p>Si on regarde le résultat le plus important de cette expérience, ce n’est pas le classement entre LLMs. C’est ça :</p>

<blockquote>
  <p>Sur la conclusion seule (C1), Sonnet 4.6 passe de 97 % à 39 %. Gemini 2.5 Pro passe de 38 % à 96 %. Sur le <em>même piège</em>, avec une simple reformulation.</p>
</blockquote>

<p>Ce n’est pas du bruit (n=120, IC95 disjoints à des kilomètres). Et ça pose un vrai problème conceptuel : une capacité de raisonnement, un “sens commun” au sens où on l’entend habituellement, devrait être invariante à la reformulation. Or, pour 6 modèles sur 8, ce n’est pas le cas. Le passage d’une question disjonctive à une question oui/non change tout.</p>

<p>Plusieurs explications peuvent être avancées, aucune n’est démontrée par cette étude seule. Je les liste comme pistes :</p>

<ul>
  <li>Possible effet d’acquiescement sur le oui/non : Sonnet 4.6 répond <em>“Oui, sans hésiter, 100 mètres c’est très court”</em> et s’aligne sur l’option proposée. Est-ce la forme interrogative qui pousse à l’acquiescement, le vocabulaire, l’ordre des éléments, ou autre chose ? Mes données ne tranchent pas.</li>
  <li>Possible appariement de motifs sur les marqueurs lexicaux : “courte distance”, “écologie”, “à pied”. Pour Llama 3.3 et GPT-4o, 90+ % des réponses commencent par cette ligne. Tentant d’y voir des tokens déclencheurs d’une heuristique pré-câblée, mais une corrélation entre entête et conclusion n’est pas une preuve de mécanisme.</li>
  <li>Possible effet de la rumination : Gemini, avec ses 1 500 tokens, arrive souvent à la bonne conclusion en fin de réponse. Opus 4.7, avec ses 400 tokens, commence souvent par “Oui à pied !” puis se ravise. Longueur associée au succès dans un cas, mais pour des modèles courts qui réussissent (GPT-5.5, 187 tokens) la relation est tout sauf monotone.</li>
  <li>Le post-training semble compter plus que la taille. Qwen3-235b-thinking (95 % C1 dans les deux variantes) vs son cousin de base (50 % C1 sur V1) à architecture, taille et budget de raisonnement comparables. C’est l’argument le plus solide, mais ce n’est pas non plus une ablation propre (les deux ne sortent pas du même entraînement avec une seule variable modifiée).</li>
  <li>Le raisonnement explicite peut compenser une partie de la fragilité au prompt. Sonnet 4.6 sans raisonnement : 97 % → 39 % entre V1 et V2. Avec <code class="language-plaintext highlighter-rouge">reasoning.effort=high</code> : 100 % → 99 % (chute disparue). Pour DeepSeek R1 c’est l’inverse : modèle entraîné à raisonner, il raisonne, et conclut faux dans les deux variantes (7-14 % C1). Le raisonnement n’est ni nécessaire ni suffisant, son effet dépend du prior.</li>
</ul>

<h3 id="ce-nest-pas-nouveau-et-ça-résiste-depuis-longtemps">Ce n’est pas nouveau (et ça résiste depuis longtemps)</h3>

<p>Cette fragilité au prompt n’est pas une découverte de mai 2026. Dans <a href="https://arxiv.org/abs/2210.14699"><em>Piloting Copilot, Codex, and StarCoder2: Hot Temperature, Cold Prompts, or Black Magic?</em></a> (Döderlein, Kouadio, Acher, Khelladi, Combemale), publié dès 2022 et étendu depuis, nous montrions sur des tâches de génération de code que des reformulations sémantiquement équivalentes du prompt produisaient des variations massives du taux de réussite, sans qu’aucune recette stable n’émerge. Quatre ans et plusieurs générations plus tard, sur une tâche de raisonnement de tous les jours, le même phénomène se reproduit, malgré l’arrivée du “reasoning by design”. Le problème semble structurel, pas conjoncturel.</p>

<p>J’ai aussi documenté ce genre d’instabilité sur d’autres tâches. Sur les échecs, <a href="https://blog.mathieuacher.com/GPT5-IllegalChessBench/">GPT-5 fait un coup illégal dès le 4e tour en moyenne</a> et <a href="https://blog.mathieuacher.com/ChessWinning7MovesGPT-copy/">les modèles GPT sont très sensibles aux en-êtes du format PGN</a>. Les <a href="https://blog.mathieuacher.com/GPTReasoningO3O4miniAndChess-copy/">limites de o3/o4-mini</a> racontent la même histoire. Je recommande fortement le <a href="https://blog.mathieuacher.com/LaParoleAuxMachinesMonsieurPhi/">livre de Monsieur Phi</a> ou sa vidéo sur <a href="https://www.youtube.com/watch?v=6D1XIbkm4JE">LLM+échecs</a>, qui montre à la fois les forces et les limites des modèles aux échecs.</p>

<p>Au-delà du code et du jeu d’échecs, de nombreux travaux montrent que les modèles sont sensiblement affectés par le prompt. 
C’est tout de même surprenenant de le constater dans le cadre du lave-auto, y compris sur des modèles récents. A surveiller dans un futur proche !</p>

<h3 id="au-final">Au final</h3>

<p>Mon intuition après lecture du jeu de données : en mode par défaut, les LLMs n’ont pas un sens commun “robuste”. Ils ont un mélange d’heuristiques apprises et de motifs lexicaux qui se cassent dès qu’on bouge la formulation. Avec raisonnement explicite, trois modèles deviennent stables entre V1 et V2 sur la conclusion : GPT-5.5 (sans qu’on le demande), Sonnet 4.6 + HIGH et Qwen3-thinking + HIGH (95-100 %). Gemini 2.5 Pro est intermédiaire : très bon sur V2 (95 %) mais seulement 58 % sur V1 même avec raisonnement HIGH. La question n’est plus exactement “ont-ils du sens commun ?”, mais “à quel point faut-il les pousser pour qu’ils l’utilisent ?”. Pour DeepSeek R1, le raisonnement ne fait rien. Et le “no excuse” reste largement non résolu.</p>

<hr />

<h2 id="6-caveat-méthodologique--sur-les-traces-de-raisonnement">6. Caveat méthodologique : sur les traces de raisonnement</h2>

<p>Un mot sur ce que disent (et ne disent pas) les compteurs de <code class="language-plaintext highlighter-rouge">reasoning_tokens</code> vus en section 3. C’est tentant d’interpréter ces compteurs comme une mesure du “temps de réflexion” du modèle, voire de sa qualité de raisonnement. Plusieurs travaux récents invitent à la prudence.</p>

<p>Kambhampati et al. plaident contre l’anthropomorphisation des tokens intermédiaires comme “pensée” du modèle (<a href="https://arxiv.org/abs/2504.09762"><em>Stop Anthropomorphizing Intermediate Tokens as Reasoning Traces</em>, 2024</a>), et montrent dans une étude associée que des traces sémantiquement <em>aberrantes</em> produisent des réponses tout aussi correctes que des traces propres (<a href="https://arxiv.org/abs/2505.13775"><em>Beyond Semantics: The Unreasonable Effectiveness of Reasonless Intermediate Tokens</em>, 2024</a>). Du côté d’Anthropic, des perturbations causales de chain-of-thought suggèrent que la réponse finale ne dépend pas toujours du raisonnement déclaré (<a href="https://www.anthropic.com/news/measuring-faithfulness-in-chain-of-thought-reasoning"><em>Measuring Faithfulness in Chain-of-Thought Reasoning</em></a>). Pour tester proprement le lien entre prompt, trace et sortie, il faut des baselines et des perturbations contrôlées (<a href="https://arxiv.org/abs/2605.01048"><em>Compared to What? Baselines and Metrics for Counterfactual Prompting</em></a>).</p>

<p>Concrètement ici : les chiffres de la section 3 renseignent sur l’effort facturé, pas sur le mécanisme. Sur ce piège, R1 raisonne longuement et conclut faux : impossible de dire à partir de cette seule expérience si c’est parce que sa chaîne de pensée tourne réellement autour du mauvais prior, ou parce que chaîne et réponse sont en partie découplées. Le piège du lave-auto n’est pas conçu pour répondre à cette question. Mais elle reste ouverte, et c’est un garde-fou utile.</p>

<hr />

<h2 id="ce-que-je-referais">Ce que je referais</h2>

<ul>
  <li>Plus de prompts inédits (générés a posteriori, pour minimiser les chances qu’ils soient dans le corpus d’entraînement).</li>
  <li>Plusieurs juges pour estimer le bruit d’évaluation. <em>Partiellement fait</em> : sur un échantillon stratifié de 128 réponses, j’ai lancé 4 sous-agents Claude indépendants en parallèle pour appliquer la même grille C1/C2. Accord avec le juge GPT-5.5 : 95.3 % sur C1 (Cohen κ = 0.904), 93.0 % sur C2 (κ = 0.792), taux de réussite agrégés quasi identiques. Les 6 désaccords C1 sont concentrés sur les réponses Opus/Sonnet de V2 (“oui à pied” puis ravisement) — cas-limite du départage interprétatif. Verdict : le juge GPT-5.5 n’est pas idiosyncratiquement biaisé sur cette tâche.</li>
  <li>Variation continue de la distance (1 m, 50 m, 500 m, 5 km) : où est le seuil de bascule de chaque modèle, et est-ce qu’il y a un <em>vrai</em> “à 5 km j’y vais en voiture parce que c’est loin”, ou bien “à 5 km j’y vais en voiture parce que c’est nécessaire” ?</li>
  <li>Autres langues (EN, DE, IT). Mode français-culturel ou universel ?</li>
</ul>

<p>Mais déjà avec 8 modèles, plusieurs conditions et plus de 4 000 réponses, l’histoire est plutôt nette :</p>

<blockquote>
  <p>Sur ce piège, en mode d’inférence par défaut, la plupart des LLMs ne montrent pas un sens commun généralisable : leurs heuristiques se cassent dès qu’on reformule la question, et le classement intra-LLM observé sur un prompt donné ne se transfère pas à une variante syntaxique pourtant équivalente sémantiquement. Avec raisonnement explicitement activé à effort élevé, plusieurs modèles récents (GPT-5.5 par défaut, Sonnet 4.6 + HIGH, Qwen3-thinking) deviennent robustes à la reformulation sur la conclusion. La fragilité est donc largement, mais pas entièrement, une affaire de mode d’inférence.</p>
</blockquote>

<p>Ce n’est pas une condamnation des LLMs. GPT-5.5 et Sonnet 4.6 sont des outils que j’utilise tous les jours et qui m’aident réellement. Le constat sur ce piège tient en trois points : GPT-5.5 est seul en avance sur le C2 (parcimonie du raisonnement), avec une robustesse qu’il atteint sans mode explicite ; les autres modèles modernes manquent de robustesse en mode par défaut, mais souvent récupèrent avec raisonnement HIGH ; les gros instruct non-reasoners (GPT-4o, Mistral, Llama, DeepSeek-chat) échouent de façon stable et le temps ne corrige pas cette classe d’erreurs.</p>

<p>Côté méthodes, un message pour le lecteur qui teste les LLMs un peu comme moi : essayer un prompt une fois, c’est très insuffisant, et <em>particulièrement</em> quand le modèle a juste. Une bonne réponse à un essai ne dit pas si elle se reproduira ni si elle survivra à une reformulation. Une mauvaise réponse, à l’inverse, suffit à identifier un problème : c’est asymétrique. Donc surtout quand “ça marche”, il faut répéter. Pour juger un modèle sur un piège donné : n significatif (10+ pour une vérification rapide, 100+ pour conclure) et au moins deux formulations sémantiquement équivalentes. Sans ça, on confond facilement le réflexe d’un essai avec une capacité installée.</p>

<h3 id="deux-questions-ouvertes">Deux questions ouvertes</h3>

<p><em>Plus de raisonnement suffit-il à avoir du sens commun ?</em> Pas mécaniquement. Le raisonnement explicite peut compenser une fragilité (Sonnet 4.6 sur V2 : 39 % → 99 % avec <code class="language-plaintext highlighter-rouge">reasoning.effort=high</code>), mais il peut aussi s’enrouler autour d’un mauvais prior (DeepSeek R1 produit ~450 tokens de raisonnement et reste à 7-14 % de bonne conclusion). On peut aussi émettre l’hypothèse que les dernières générations de modèles sont plus robustes de par leur raisonnement.</p>

<p><em>Les modèles “frontier” ont-ils été “patchés” pour passer ce test précis ?</em> C’est l’autre hypothèse. Mes données ne le prouvent pas et il faut rester prudent. Mais elles sont compatibles avec l’hypothèse : la lignée GPT-5 conclut juste depuis août 2025 ; ce qui s’améliore monotoniquement sur 9 mois (gpt-5 → 5.1 → 5.5) c’est précisément la parcimonie sur le critère “ne pas mentionner la distance”, exactement le détail qui rend ce piège visible plutôt qu’un autre. Le test est connu sur les réseaux depuis 2024, un fine-tuning ciblé serait plausible sans être avéré. Pour trancher, il faudrait tester des variantes inédites de la structure du piège: c’est dans la liste des “à faire” ;-)</p>

<hr />

<p><em>Toutes les données, prompts, et scripts de cette manip : <a href="https://github.com/acherm/laveauto-llm">github.com/acherm/laveauto-llm</a>.</em></p>

<p><em>Setup technique : Python 3 + openai SDK async, ~25 appels concurrents sur OpenRouter. Coût total ~quelques euros. Juge = GPT-5.5 à température 0, deux verdicts indépendants par réponse. Toutes les expériences sans system prompt, juste la question en message utilisateur.</em></p>

<p><em>Un avis, une variante de prompt à tester, un désaccord ? mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026llmcarwashfr</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{Le test du lave-auto : les LLMs ont-ils vraiment du sens commun ?}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{may}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/LLMCarWashCommonSense/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/LLMCarWashCommonSense/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="LLM" /><category term="common sense" /><category term="OpenRouter" /><category term="benchmark" /><category term="GPT-5" /><category term="Claude" /><category term="Gemini" /><category term="DeepSeek" /><category term="Qwen" /><category term="reasoning" /><category term="prompt engineering" /><category term="prompt sensitivity" /><summary type="html"><![CDATA[L’exemple est devenu un classique : “Je dois laver ma voiture. Le centre de lavage est à 100 m. J’y vais à pied ou en voiture ?”. Réponse de sens commun : en voiture, évidemment, sinon la voiture n’est pas là pour être lavée. Sauf que la plupart des LLMs tombent dans le panneau et recommandent la marche pour des raisons d’écologie ou de courte distance.]]></summary></entry><entry><title type="html">A PhD Defense on Legacy Software Modernization: from Esope to Fortran 2008</title><link href="http://blog.mathieuacher.com/PhDDefenseLegacySoftwareModernizationEsopeFortran/" rel="alternate" type="text/html" title="A PhD Defense on Legacy Software Modernization: from Esope to Fortran 2008" /><published>2026-03-24T09:54:29+00:00</published><updated>2026-03-24T09:54:29+00:00</updated><id>http://blog.mathieuacher.com/PhDDefenseLegacySoftwareModernizationEsopeFortran</id><content type="html" xml:base="http://blog.mathieuacher.com/PhDDefenseLegacySoftwareModernizationEsopeFortran/"><![CDATA[<p>I had the pleasure of serving as a reviewer for Younoussa Sow’s PhD defense in Lille, on a topic that is both challenging and fascinating: the automatic migration of Esope to Fortran 2008. This CIFRE PhD, carried out with <a href="https://www.framatome.com/">Framatome</a> and co-supervised by <a href="https://chercheurs.lille.inria.fr/~nanqueti/nicolasAnquetil.html">Nicolas Anquetil</a> and <a href="http://stephane.ducasse.free.fr/">Stéphane Ducasse</a>, tackles a major modernization challenge: migrating around one million lines of scientific code built on Esope, a proprietary extension on top of Fortran 77.</p>

<p>What makes this work especially difficult is that the Esope ecosystem is closed and “legacy”. 
That makes parsing, transforming, and modernizing the code particularly hard, from a software engineering perspective.
The defense strongly resonated with my own current interests in RPG and COBOL modernization, and more broadly with the challenges posed by legacy or unusual/esoteric languages. No LLMs in this thesis, but many questions that connect with ongoing discussions around software migration and modernization, including in the context of LLM4Code.</p>

<p>It was also a great opportunity to exchange, the day before, on these topics. The defense was a reminder of two important things. First, how valuable it is to attend PhD defenses in person, to better understand the story, context, collaboration dynamics, and difficulties behind the work. Second, how essential computer science is to the evolution of critical systems across many domains… and how much we need computer scientists to make these systems evolve.</p>

<p>Two major publications from the thesis:</p>
<ul>
  <li>Younoussa Sow, Nicolas Anquetil, Léandre Brault, Stéphane Ducasse – Migrating Esope to Fortran 2008 using model transformations (SANER 2026)</li>
  <li>Younoussa Sow, Larisa Safina, Léandre Brault, Papa Ibou Diouf, Stéphane Ducasse, Nicolas Anquetil – Parsing Fortran-77 with proprietary extensions (ICSME 2023, pp. 453–462)</li>
</ul>

<p>Congratulations to Younoussa Sow for the defense, and thanks to the whole team for the discussions!</p>

<p><em>(new!) I realized that some of my blog post entries are sometimes cited in academic works, so why not using the following bibtex entry?</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026phddefenseesope</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{A PhD Defense on Legacy Software Modernization: from Esope to Fortran 2008}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/PhDDefenseLegacySoftwareModernizationEsopeFortran/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/PhDDefenseLegacySoftwareModernizationEsopeFortran/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="legacy software" /><category term="modernization" /><category term="Fortran" /><category term="Esope" /><category term="parsing" /><category term="software migration" /><category term="PhD defense" /><category term="LLM4Code" /><summary type="html"><![CDATA[I had the pleasure of serving as a reviewer for Younoussa Sow’s PhD defense in Lille, on a topic that is both challenging and fascinating: the automatic migration of Esope to Fortran 2008. This CIFRE PhD, carried out with Framatome and co-supervised by Nicolas Anquetil and Stéphane Ducasse, tackles a major modernization challenge: migrating around one million lines of scientific code built on Esope, a proprietary extension on top of Fortran 77.]]></summary></entry><entry><title type="html">BFChess: A Chess Engine in Brainfuck, Built by a Coding Agent</title><link href="http://blog.mathieuacher.com/BFChessChessEngineBrainfuck/" rel="alternate" type="text/html" title="BFChess: A Chess Engine in Brainfuck, Built by a Coding Agent" /><published>2026-03-23T13:00:00+00:00</published><updated>2026-03-23T13:00:00+00:00</updated><id>http://blog.mathieuacher.com/BFChessChessEngineBrainfuck</id><content type="html" xml:base="http://blog.mathieuacher.com/BFChessChessEngineBrainfuck/"><![CDATA[<p><a href="https://github.com/acherm/agentic-chessengine-brainfuck">BFChess</a> is a UCI-compatible chess engine written entirely in <a href="https://en.wikipedia.org/wiki/Brainfuck">Brainfuck</a>. The generated engine is 5.6 MB of raw Brainfuck code (eight distinct characters: <code class="language-plaintext highlighter-rouge">&gt;&lt;+-.,[]</code>), produced by a 7,400-line Python compiler. Here is what a small excerpt of the engine looks like:
<img src="/assets/bfchess-snippet.png" alt="A snippet of chess.bf: 5.6 MB of raw Brainfuck code made of eight characters" />
It implements depth-3 minimax search with alpha-beta pruning, full move generation (including castling, en passant, and promotion), and MVV-LVA evaluation with positional bonuses. It passes 11/11 perft validation positions. It is not strong: it beats random moves convincingly but loses every game against Stockfish at minimum settings. Each move takes between 45 seconds and 10 minutes to compute. The project shows that coding agents can operate in Brainfuck at a scale never achieved before for a chess engine, not through raw translation but by designing intermediate abstractions (a pointer-tracking emitter, a memory layout manager, runtime loop patterns) that make the task tractable. Yet despite a non-trivial implementation of search, evaluation, and special moves, the resulting engine is only capable of beating a random move generator, with a large gap to even the weakest conventional engines. Closing that gap, through human-AI co-design iteration, is an open and exciting direction.</p>

<p>Brainfuck has no variables, no functions, no arithmetic beyond increment/decrement, and no random memory access. Its only data structure is a linear tape of byte-sized cells. It is Turing-complete, so in theory one can write a chess engine in it. In practice, no one appears to have done so before (see <a href="#novelty-has-this-been-done-before">Novelty</a> below).</p>

<p>This work is part of a broader experiment where I ask coding agents to build chess engines from scratch across <a href="https://blog.mathieuacher.com/FromScratchChessEnginesPolyglot/">many programming languages</a>. The <a href="https://blog.mathieuacher.com/TeXCCChessEngine/">TeX engine</a> plays at roughly the level of a casual tournament player. Brainfuck is a significantly harder target, and the results reflect that.</p>

<h2 id="novelty-has-this-been-done-before">Novelty: Has This Been Done Before?</h2>

<p>A fully functioning, openly available chess engine written in Brainfuck does not appear to exist in durable public form as of February 2026. The best-documented serious attempt was a 2019 community project announced on <a href="https://talkchess.com/">TalkChess</a>, with an intended design described as “a simplified micro-Max” and an initial GitHub repository named <em>chessfuck</em>. The repository link publicly shared in 2019 is now dead (HTTP 404), strongly suggesting deletion or privatization; the surviving technical record is limited to the TalkChess discussion thread itself.</p>

<p>What does exist, with primary sources, are (a) chess-adjacent Brainfuck utilities such as <code class="language-plaintext highlighter-rouge">chessboard.b</code>, which renders an ASCII chessboard from a FEN position string (Daniel B. Cristofani, brainfuck.org), and (b) small-board game implementations with AI (notably tic-tac-toe), which demonstrate the kinds of state encoding, control-flow tricks, and sentinel-based “subroutine” patterns that would be required to scale toward chess-like complexity.</p>

<p>Two “enablers” appear repeatedly in the chess-in-BF discussion: speed improvements via optimized interpreters and JITs (e.g., Eli Bendersky’s 2017 BF JIT series), and hardware BF execution (e.g., a 256-core BF processor paper). These do not solve the human-factor problem of writing and maintaining BF at chess-engine scale, but they do change feasibility assumptions around runtime performance.</p>

<p>BFChess appears to be the first complete, publicly available implementation.</p>

<h2 id="why-brainfuck">Why Brainfuck?</h2>

<p>Because it represents an extreme point on the language-capability spectrum. If a coding agent can produce a working chess engine in Brainfuck, that tells us something about the boundaries of what these tools can handle in severely constrained language environments. Unlike TeX (which at least has integer registers and macro expansion), Brainfuck offers essentially nothing to build on. Every abstraction must be constructed from scratch, using only pointer movement and cell increment/decrement.</p>

<h2 id="architecture-a-python-to-bf-compiler">Architecture: A Python-to-BF Compiler</h2>

<p>BFChess is not hand-written Brainfuck. Instead, a Python compiler (<code class="language-plaintext highlighter-rouge">generate.py</code> and ~7,400 lines of supporting modules) emits the 5.6 MB <code class="language-plaintext highlighter-rouge">chess.bf</code> file. The compiler tracks a virtual data pointer at generation time, so it can emit minimal <code class="language-plaintext highlighter-rouge">&gt;</code> and <code class="language-plaintext highlighter-rouge">&lt;</code> sequences to navigate between memory cells. It is, in effect, a code generator targeting an extremely constrained instruction set.</p>

<table>
  <thead>
    <tr>
      <th>Module</th>
      <th style="text-align: right">Lines</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_movegen.py</code></td>
      <td style="text-align: right">4,470</td>
      <td>Move generation, search, evaluation</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_chess.py</code></td>
      <td style="text-align: right">498</td>
      <td>Board ops, UCI position parsing, move application</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_memory.py</code></td>
      <td style="text-align: right">262</td>
      <td>Memory layout (672 cell addresses)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_emitter.py</code></td>
      <td style="text-align: right">185</td>
      <td>Core BF emitter with pointer tracking</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_uci.py</code></td>
      <td style="text-align: right">184</td>
      <td>UCI protocol loop</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_primitives.py</code></td>
      <td style="text-align: right">182</td>
      <td>Control flow (<code class="language-plaintext highlighter-rouge">compare_eq</code>, <code class="language-plaintext highlighter-rouge">switch_on_value</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bf_io.py</code></td>
      <td style="text-align: right">108</td>
      <td>I/O (stdin line reading, output)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">generate.py</code></td>
      <td style="text-align: right">39</td>
      <td>Entry point</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bfi.c</code></td>
      <td style="text-align: right">177</td>
      <td>RLE-optimized BF interpreter</td>
    </tr>
    <tr>
      <td><strong>Total</strong></td>
      <td style="text-align: right"><strong>~7,400</strong></td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>The generated <code class="language-plaintext highlighter-rouge">chess.bf</code> is <strong>5.6 MB</strong> of raw Brainfuck. An RLE-optimized interpreter (<code class="language-plaintext highlighter-rouge">bfi.c</code>) compresses it at load time from ~3.8M characters to ~520K instructions (7x compression), with a precomputed jump table for O(1) bracket matching.</p>

<h3 id="memory-layout">Memory Layout</h3>

<p>The engine uses 672 cells on the BF tape, each with a carefully assigned role:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Cells 0-15:      Temporaries
 Cells 16-29:     Game state (side to move, castling rights, king positions, EP file)
 Cells 23-28:     Move generation hot cells (BEST_FROM, BEST_TO, HAVE_LEGAL)
 Cells 30-93:     Chess board (64 squares)
 Cells 100-227:   UCI input buffer (128 bytes)
 Cells 120-129:   Legality workspace (make/unmake)
 Cells 130-159:   Evaluation (scoring, attack detection, exchange, check)
 Cells 160-183:   Depth-2/3 search control
 Cells 400-471:   Depth-2 state backup (64 board + 8 state cells)
 Cells 600-671:   Depth-3 state backup
</code></pre></div></div>

<h3 id="the-fundamental-problem-no-random-access">The Fundamental Problem: No Random Access</h3>

<p>Brainfuck has no <code class="language-plaintext highlighter-rouge">board[index]</code> where <code class="language-plaintext highlighter-rouge">index</code> is a runtime value. To read a square whose index is computed at runtime, the engine must execute a <strong>64-way switch</strong>: compare the index against 0, 1, 2, …, 63 and copy from the matching fixed cell. This single constraint is the dominant cost driver. Every board read or write is ~20 KB of BF code: a 64-way cascade of comparisons and pointer movements.</p>

<p>This is what makes Brainfuck fundamentally different from TeX (which has 32K indexed <code class="language-plaintext highlighter-rouge">\count</code> registers) or even <a href="https://blog.mathieuacher.com/CodingAgentsMnMLang/">M&amp;M’s language</a> (which at least has named variables).</p>

<h2 id="features">Features</h2>

<p>Despite the language constraints, BFChess implements:</p>

<ul>
  <li><strong>Depth-3 minimax search</strong> with alpha-beta pruning and captures-first move ordering</li>
  <li><strong>Full move generation</strong> for all piece types, including castling (kingside/queenside), en passant, and promotion</li>
  <li><strong>MVV-LVA evaluation</strong> with positional bonuses: center control, piece activity, pawn advancement, check detection, exchange awareness, anti-repetition penalty</li>
  <li><strong>3-pass attack analysis</strong> for legality checking, destination safety, and check detection, with zero code duplication (a single <code class="language-plaintext highlighter-rouge">is_attacked()</code> routine wrapped in a 3-iteration BF loop)</li>
  <li><strong>Stalemate detection</strong> at all search depths</li>
  <li><strong>Perft validation</strong>: 11/11 positions correct (including castling and EP edge cases)</li>
  <li><strong>UCI protocol</strong>: <code class="language-plaintext highlighter-rouge">uci</code>, <code class="language-plaintext highlighter-rouge">isready</code>, <code class="language-plaintext highlighter-rouge">position startpos</code>, <code class="language-plaintext highlighter-rouge">domove</code>, <code class="language-plaintext highlighter-rouge">go</code>, <code class="language-plaintext highlighter-rouge">perft</code>, <code class="language-plaintext highlighter-rouge">quit</code></li>
</ul>

<h3 id="size-optimization-from-119-mb-to-56-mb">Size Optimization: From 119 MB to 5.6 MB</h3>

<p>The naive approach (unrolling every board access at compile time) produced a 119 MB BF program. Four optimization strategies brought it down:</p>

<ol>
  <li><strong>Runtime loops</strong> instead of compile-time unrolling (117 MB -&gt; 1.7 MB)</li>
  <li><strong>Runtime offset dispatch</strong> for knight/king/sliding piece movement (1.7 MB -&gt; 282 KB)</li>
  <li><strong>Cell proximity</strong>: placing frequently co-accessed cells near each other to minimize pointer travel</li>
  <li><strong>Efficient encoding</strong>: values &gt;128 use decrements from 0 (2 characters) instead of 128+ increments</li>
</ol>

<p>The depth-1 engine was 943 KB. Adding depth-2 and depth-3 search (each requiring full 72-cell state save/restore) brought the final size to 5.6 MB.</p>

<h2 id="how-strong-is-it">How Strong Is It?</h2>

<p>Short answer: not very. But it plays legal chess and beats random moves convincingly.</p>

<h3 id="vs-stockfish-18-minimum-settings">vs Stockfish 18 (minimum settings)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Results: +0 =0 -10 / 10 (0%)
All 10 losses by checkmate (12-32 moves)
</code></pre></div></div>

<p>BFChess captures material and controls the center in the opening, but 3 plies of search is not enough to see the tactical threats Stockfish exploits. Here is a typical game:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. d4 d5 2. e4 c6 3. Nc3 a6 4. Qf3 dxe4 5. Nxe4 Nd7 6. d5 Qa5+
7. Bd2 Qxd5 8. Nf6+ exf6 9. Qxd5 cxd5 10. Bxa6 ...
</code></pre></div></div>

<p>BFChess plays reasonable opening moves (d4, e4, Nc3) and grabs material when it can, but lacks the ability to defend against multi-move threats.</p>

<h3 id="vs-random-legal-moves">vs Random Legal Moves</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Results: +4 =6 -0 / 10 (70%)
4 wins by checkmate, 6 draws by stalemate, 0 losses
</code></pre></div></div>

<p>It dominates random play in the opening/middlegame, but 60% of games end in stalemate: the engine captures everything and accidentally traps the bare king. Stalemate positions forming beyond the 3-ply horizon remain invisible.</p>

<h3 id="summary">Summary</h3>

<table>
  <thead>
    <tr>
      <th>Opponent</th>
      <th>Score</th>
      <th>Win%</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Random legal moves</td>
      <td>+4 =6 -0</td>
      <td>70%</td>
    </tr>
    <tr>
      <td>Stockfish (minimum settings)</td>
      <td>+0 =0 -10</td>
      <td>0%</td>
    </tr>
  </tbody>
</table>

<p>The sample sizes are too small (10 games each) and the win rates too extreme (0% and 70%) for a meaningful Elo estimate. What we can say: BFChess is clearly stronger than random play, but clearly weaker than even the weakest Stockfish. For context, the <a href="https://blog.mathieuacher.com/TeXCCChessEngine/">TeX engine</a> plays at roughly the level of a casual tournament player, with quiescence search and piece-square tables. The gap is real and largely comes from depth and evaluation quality.</p>

<h2 id="what-would-it-take-to-improve">What Would It Take to Improve?</h2>

<p>The main weaknesses are clear:</p>

<ul>
  <li><strong>No quiescence search</strong>: the engine can’t see beyond the fixed 3-ply horizon, so it routinely walks into tactical threats</li>
  <li><strong>No piece-square tables</strong>: evaluation is crude (center bonus + MVV-LVA), without the positional nuance that piece-square tables provide</li>
  <li><strong>No iterative deepening or time management</strong>: every position gets the same fixed-depth search, whether it takes 45 seconds or 10 minutes</li>
  <li><strong>No opening book or endgame tablebase</strong></li>
  <li><strong>Stalemate blindness</strong>: a major practical issue against weaker opponents</li>
</ul>

<p>Adding quiescence search would likely have the biggest impact, but in Brainfuck, every additional search depth multiplies code size (each depth level needs its own 72-cell state backup region) and execution time. A single move already takes 45 to 600+ seconds on the RLE-optimized interpreter. The challenge is not just algorithmic: Brainfuck’s computational overhead makes deeper search extremely expensive.</p>

<p>There is also a meta-challenge: <strong>assessing the engine is itself costly</strong>. Each 10-game tournament against Stockfish takes several hours of wall-clock time. Iterating on the evaluation function or search algorithm means regenerating the 5.6 MB BF file, then waiting hours for results. The feedback loop is orders of magnitude slower than for a normal chess engine, where one can run thousands of games overnight.</p>

<h2 id="the-effort-sessions-tokens-and-cost">The Effort: Sessions, Tokens, and Cost</h2>

<p>BFChess was built using <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> powered by Claude Opus 4.6. Here is what the development looked like:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Calendar span</td>
      <td>30 days (Feb 21 - Mar 23, 2026)</td>
    </tr>
    <tr>
      <td>Active working time</td>
      <td>~34 hours</td>
    </tr>
    <tr>
      <td>Sessions</td>
      <td>17</td>
    </tr>
    <tr>
      <td>User messages</td>
      <td>~1,973</td>
    </tr>
    <tr>
      <td>Total API calls</td>
      <td>~2,700 (main + subagents)</td>
    </tr>
    <tr>
      <td>Total tokens</td>
      <td>~221M (mostly cache reads)</td>
    </tr>
    <tr>
      <td>Estimated API cost</td>
      <td>~$758 (at standard Opus pricing)</td>
    </tr>
    <tr>
      <td>Git commits</td>
      <td>10</td>
    </tr>
  </tbody>
</table>

<p>The token count is dominated by cache reads (~198M tokens): Claude Code reloads context at each turn, so the same codebase gets re-read hundreds of times. The actual novel output is ~344K tokens. At standard Anthropic API pricing, that’s about $758, though with a Claude Max subscription the cost structure is different (flat monthly fee).</p>

<h3 id="what-kind-of-instructions-were-given">What Kind of Instructions Were Given?</h3>

<p>The approach was deliberately iterative. I did not hand Claude Code a specification. Instead, the development went roughly like this:</p>

<ol>
  <li><strong>“Build a chess engine in Brainfuck”</strong>: the initial prompt. Claude Code produced the first working UCI engine (733 KB) in the first session.</li>
  <li><strong>“Add depth-2 search, then depth-3”</strong>: progressive deepening, each requiring new state management.</li>
  <li><strong>Bug reports from testing</strong>: most of my messages were reporting bugs found during Stockfish tournaments (cell collisions, wrong flag cells, stale values). Claude Code would diagnose and fix them, sometimes discovering that a “fix” introduced a new cell collision.</li>
  <li><strong>“Run perft and compare to python-chess”</strong>: verification-driven development. Perft caught several move generation bugs early.</li>
  <li><strong>“Add castling”, “Add en passant”</strong>: feature requests, each requiring extensive make/unmake support.</li>
  <li><strong>“Play 10 games against Stockfish and show me the PGN”</strong>: assessment loops.</li>
</ol>

<p>The hardest bugs were <strong>cell collisions</strong>: multiple code paths accidentally writing to the same memory cell. In a normal language, you’d get a clear error. In Brainfuck, one routine silently corrupts another routine’s state, and the symptom shows up moves later. Claude Code found and fixed at least 9 such bugs across the development, including one where the <code class="language-plaintext highlighter-rouge">print_char</code> function was using cell 30 (= <code class="language-plaintext highlighter-rouge">BOARD_START</code>) as scratch space, corrupting the board every time any text was printed.</p>

<h3 id="the-meta-problem-feedback-loop-cost">The Meta-Problem: Feedback Loop Cost</h3>

<p>The real bottleneck was not writing the code, but evaluating it. Each iteration of “change something, regenerate chess.bf, play games, see if it improved” takes hours. Some representative times:</p>

<ul>
  <li>Generating <code class="language-plaintext highlighter-rouge">chess.bf</code>: ~1-2 minutes</li>
  <li>One move from the engine: 45 seconds (opening) to 600+ seconds (complex middlegame)</li>
  <li>A 10-game tournament: 3-8 hours</li>
  <li>A complete test-fix-retest cycle: half a day</li>
</ul>

<p>This means the engine has seen far fewer iterations than a normal chess engine development project would involve. There is almost certainly low-hanging fruit in the evaluation function, but finding it requires patience and compute.</p>

<h2 id="what-does-this-show">What Does This Show?</h2>

<p>As a chess engine, BFChess has no practical value: it is too slow and too weak. But as an artifact, it raises several points worth noting.</p>

<p><strong>On language boundaries.</strong> This appears to be the first publicly available chess engine in Brainfuck. Chess engines have been written in C, Java, Rust, JavaScript, COBOL, TeX, and many other languages, but the esoteric end of the spectrum was largely unexplored. BFChess shows that the language boundary for chess engines extends to extremely minimal Turing-complete languages. The bottleneck is not expressiveness per se (Turing-completeness guarantees that), but the practical cost of encoding complex state management (672 cells, zero collisions) and search algorithms (3-ply minimax with full board save/restore) in a language with no abstraction facilities.</p>

<p><strong>On coding agents.</strong> Building this required Claude Code to manage a flat memory layout across ~7,400 lines of code generator, reason about pointer arithmetic and cell allocation, debug silent corruption bugs where one routine overwrites another’s state, and optimize code size from 119 MB to 5.6 MB. The hardest class of bugs (cell collisions) would be difficult for a human to track manually at this scale, yet the agent found and fixed at least 9 of them across 17 sessions. At the same time, the agent could not independently assess the quality of its output: it needed the human to run tournaments, report failures, and guide the iteration. The division of labor was roughly: the agent writes and debugs code, the human steers priorities and evaluates results.</p>

<p><strong>On staying in pure Brainfuck.</strong> A parallel attempt was made with <a href="https://github.com/openai/codex">Codex</a> (GPT-5.3) to build the same kind of engine. The result is instructive: the repository contains real Brainfuck kernels, but upon inspection the live engine also relies on Python for search (<code class="language-plaintext highlighter-rouge">src/search.py</code>) and several board-operation fast paths (<code class="language-plaintext highlighter-rouge">src/bf_board.py</code>). The Brainfuck components (<code class="language-plaintext highlighter-rouge">src/brainfuck_selector.bf</code>) are kept for backward compatibility but are not the active engine core. In other words, the agent quietly migrated critical logic back into Python, presumably to optimize for convenience or speed. The constraint of staying in pure Brainfuck was not enforced explicitly enough, and the agent did not maintain it on its own. This is not a comment on the quality of Codex as an agent, but it illustrates two broader points: (1) keeping all engine logic in Brainfuck is genuinely difficult, and agents under pressure to produce working code will naturally gravitate toward more expressive languages when given the opportunity; (2) when working with coding agents on constrained tasks, the human must actively verify that the constraints are respected, because the agent may “solve” the problem by relaxing them. This pattern is not unique to Brainfuck: a similar drift was observed in the <a href="https://blog.mathieuacher.com/PrintfOrientedProgrammingCodingAgents/">printf-oriented programming experiment</a>, where agents tasked with implementing logic purely through <code class="language-plaintext highlighter-rouge">printf</code> format strings would occasionally fall back to conventional C code. In the BFChess project with Claude Code, the constraint held because the architecture (Python compiler emitting BF, with the engine running solely through <code class="language-plaintext highlighter-rouge">./bfi chess.bf</code>) made it structurally impossible to smuggle Python logic into the runtime. The Codex attempt is documented in <a href="https://github.com/acherm/agentic-chessengine-brainfuck-codexfailure">its own repository</a> for those interested in the details.</p>

<p><strong>On feasibility and cost.</strong> The project consumed ~221M tokens (~$758 at API pricing) and ~34 hours of active work over 30 days. Most of that time was spent on the feedback loop (regenerate, test, diagnose), not on writing code. This suggests that for Brainfuck-scale complexity, the main barrier to improvement is not the agent’s coding ability but the cost of evaluation: each iteration takes hours of wall-clock time, limiting how many hypotheses can be tested.</p>

<p><strong>As a starting point.</strong> The architecture (Python compiler to BF engine) and the infrastructure (perft tests, Stockfish tournaments, RLE interpreter) are all in place. Improving the engine from here is a matter of adding features (quiescence search, piece-square tables, better move ordering), which is difficult but incremental.</p>

<h2 id="trying-it">Trying It</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/acherm/agentic-chessengine-brainfuck
<span class="nb">cd </span>chess-brainfuck-cc
make                <span class="c"># builds bfi + generates chess.bf</span>
<span class="nb">printf</span> <span class="s1">'uci\nisready\nposition startpos\ngo\nquit\n'</span> | ./bfi chess.bf
<span class="c"># -&gt; bestmove d2d4</span>
</code></pre></div></div>

<p>You can play interactively with <code class="language-plaintext highlighter-rouge">python3 play.py</code> or run tournaments with <code class="language-plaintext highlighter-rouge">python3 play_stockfish.py</code>. Patience is required.</p>

<h2 id="summary-1">Summary</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>BFChess</th>
      <th><a href="https://blog.mathieuacher.com/TeXCCChessEngine/">TeXCCChess</a></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Language</td>
      <td>Brainfuck</td>
      <td>TeX</td>
    </tr>
    <tr>
      <td>Engine size</td>
      <td>5.6 MB (raw BF)</td>
      <td>~2,100 lines TeX</td>
    </tr>
    <tr>
      <td>Compiler</td>
      <td>7,400 lines Python</td>
      <td>N/A (direct TeX)</td>
    </tr>
    <tr>
      <td>Search depth</td>
      <td>3 (minimax + alpha-beta)</td>
      <td>3 (negamax + alpha-beta + quiescence)</td>
    </tr>
    <tr>
      <td>vs Stockfish (min)</td>
      <td>0/10</td>
      <td>Competitive</td>
    </tr>
    <tr>
      <td>Move time</td>
      <td>45-600+ seconds</td>
      <td>2-30 seconds</td>
    </tr>
    <tr>
      <td>Development cost</td>
      <td>~$758 / 221M tokens / 34h</td>
      <td>TBD</td>
    </tr>
  </tbody>
</table>

<p>BFChess is weaker, slower, and harder to improve than TeXCCChess. But it exists: a complete, legal-move-generating, UCI-speaking, depth-3-searching chess engine in a language with eight instructions and no memory abstraction. It is a solid starting point and, to the best of our knowledge, the first of its kind. Making it stronger will require more work, more compute, and likely some creative rethinking of what is feasible within Brainfuck’s severe constraints.</p>

<p><em>BFChess was developed by <a href="https://mathieuacher.com">Mathieu Acher</a> and <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> (Opus 4.6). Source code: <a href="https://github.com/acherm/agentic-chessengine-brainfuck">https://github.com/acherm/agentic-chessengine-brainfuck</a></em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026bfchess</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{BFChess: A Chess Engine in Brainfuck, Built by a Coding Agent}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/BrainfuckChessEngine/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/BrainfuckChessEngine/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="chess" /><category term="chess engine" /><category term="LLM" /><category term="coding agents" /><category term="Brainfuck" /><category term="esoteric languages" /><category term="software engineering" /><category term="generative AI" /><category term="llm4code" /><category term="Claude Code" /><summary type="html"><![CDATA[BFChess is a UCI-compatible chess engine written entirely in Brainfuck. The generated engine is 5.6 MB of raw Brainfuck code (eight distinct characters: &gt;&lt;+-.,[]), produced by a 7,400-line Python compiler. Here is what a small excerpt of the engine looks like: It implements depth-3 minimax search with alpha-beta pruning, full move generation (including castling, en passant, and promotion), and MVV-LVA evaluation with positional bonuses. It passes 11/11 perft validation positions. It is not strong: it beats random moves convincingly but loses every game against Stockfish at minimum settings. Each move takes between 45 seconds and 10 minutes to compute. The project shows that coding agents can operate in Brainfuck at a scale never achieved before for a chess engine, not through raw translation but by designing intermediate abstractions (a pointer-tracking emitter, a memory layout manager, runtime loop patterns) that make the task tractable. Yet despite a non-trivial implementation of search, evaluation, and special moves, the resulting engine is only capable of beating a random move generator, with a large gap to even the weakest conventional engines. Closing that gap, through human-AI co-design iteration, is an open and exciting direction.]]></summary></entry><entry><title type="html">Corners, Free Kicks, and Set Pieces Across Europe’s Top Football Leagues: What the Data Actually Says</title><link href="http://blog.mathieuacher.com/CornersSetPiecesFootballEN/" rel="alternate" type="text/html" title="Corners, Free Kicks, and Set Pieces Across Europe’s Top Football Leagues: What the Data Actually Says" /><published>2026-03-18T11:00:00+00:00</published><updated>2026-03-18T11:00:00+00:00</updated><id>http://blog.mathieuacher.com/CornersSetPiecesFootballEN</id><content type="html" xml:base="http://blog.mathieuacher.com/CornersSetPiecesFootballEN/"><![CDATA[<p>Corners and set pieces are one of football’s most debated topics. “Arsenal are unstoppable from corners,” “Marseille are hopeless on free kicks,” and so on. Opinions, gut feelings, conventional wisdom. But sometimes the data surprises: did you know that Tottenham, fighting relegation in the Premier League, are on the European podium for corner goals this season, just behind Arsenal and Inter?
What if we actually checked?
I analyzed <strong>5 seasons of data</strong> across Europe’s top 5 leagues (Premier League, Ligue 1, Serie A, Bundesliga, La Liga). Every shot, every goal, every expected goal (xG) from corners, indirect free kicks, and direct free kicks. 484 team-seasons. Here’s what I found in 10 key takeaways, and some results are surprising, underappreciated, or counter-intuitive.</p>

<p><em>Data: Understat via understatapi. 5 European leagues, 5 seasons (2021-2026), 484 team-seasons. Note: the 2025-26 season is ongoing (~26-30 matchdays depending on the league). Trends are clear but figures will evolve. I’ll update at the end of the season.</em></p>

<h2 id="1-tottenham-kings-of-corners-and-16th-in-the-premier-league">1. Tottenham, kings of corners… and 16th in the Premier League</h2>

<p>This is the wildest story of the current season (~30 PL matchdays played). Tottenham are <strong>3rd in Europe</strong> for goals scored from corners: 14 goals, behind Arsenal (16) and Inter (15). A 21.5% conversion rate, the best in Europe.</p>

<p>The problem? Spurs are currently <strong>16th in the Premier League</strong> with 30 points.</p>

<p><img src="/assets/en_tottenham_open_vs_corners.png" alt="Tottenham vs Arsenal: Open Play vs Corner Goals" /></p>

<p>The chart speaks for itself. Tottenham are isolated in the top left: lots of corner goals, very few from open play (25 goals, 14th in the league). Arsenal sit in the top right: 16 corner goals AND 39 from open play.</p>

<p><strong>35.0% of Tottenham’s goals come from set pieces.</strong> That’s the highest rate among major league clubs (excluding relegation-zone teams). When over a third of your goals depend on situations that account for ~15% of all shots, you’re building on sand.</p>

<p>For comparison, Bayern Munich, Europe’s best attack, are at just 15.6% set piece dependency. Because when you score 65 open play goals, corners are a bonus, not a crutch.</p>

<p>And then there’s Arsenal, the outlier: <strong>16 corner goals (1st in Europe)</strong>, plus 39 from open play, 70 points (1st in the Premier League). Arsenal aren’t just lethal offensively from set pieces, they dominate defensively too. In their Champions League match against Bayer Leverkusen, <a href="https://x.com/bayer04_en/status/2031723079886401891">Bayer 04’s official account</a> noted that Arsenal conceded zero corners. In a <a href="https://talksport.com/football/4043546/john-obi-mikel-interview-arsenal-chelsea-mourinho-nigeria/">controversial interview</a>, John Obi Mikel went as far as accusing Arsenal of cheating or suggesting their success depends solely on corners. Tottenham are the perfect counter-example: there’s far more to football than corners.
One could also argue Tottenham are in serious danger: without set pieces, their situation would be even worse than 16th place.</p>

<hr />

<h2 id="2-elche-2022-23-107-corner-shots-the-anatomy-of-desperation">2. Elche 2022-23: 107 corner shots, the anatomy of desperation</h2>

<p>Beyond Tottenham in 2025-26, there are other extreme cases. Elche, 2022-23 season, La Liga. The team was relegated with 25 points, yet they fired <strong>107 shots from corners</strong>, a La Liga record that season.</p>

<p><img src="/assets/en_elche_corners_2022_23.png" alt="Corner shot distribution, La Liga 2022-23" /></p>

<p>The z-score is 2.26, a statistical outlier. But the right-hand chart is even more telling: <strong>26.4% of all Elche’s shots came from corners</strong>. The La Liga average is 15.6%. No one else exceeds 19%.</p>

<p>Why? Because Elche couldn’t do anything else. Just 406 total shots (the lowest in La Liga), non-existent open play creation. Corners were their only route to goal. Lucas Boye, their target man, took 19 corner shots alone, shouldering the entire aerial workload.</p>

<p>When you can’t do anything else, you’re left with corners. Tottenham 2025-26 is a less extreme version of the same syndrome.</p>

<p>For the record, I haven’t watched a single Elche match. But the statistics are quite clear (I also checked whether this was a data anomaly or a single misrecorded match inflating the count, but no). <strong>I’m looking for Elche followers or supporters, because this is a truly remarkable phenomenon</strong>, and despite some searching, I haven’t found anything documenting this Elche exploit.</p>

<hr />

<h2 id="3-marseille-from-shame-to-partial-renaissance-on-corners">3. Marseille, from shame to (partial) renaissance on corners</h2>

<p>In November 2025, <a href="https://x.com/Rubenzf911/status/1985468834401116253">@Rubenzf911</a> asked the question many Marseille supporters had on their minds:</p>

<blockquote>
  <p><em>“OM’s set pieces, is anyone ever going to sound the alarm?”</em></p>
</blockquote>

<p>The alarm, the data had been ringing it for a while. Here’s Marseille’s evolution over 5 seasons:</p>

<p><img src="/assets/en_table_marseille_evolution.png" alt="Marseille evolution, 5 seasons" /></p>

<p>The table tells a three-act story:</p>

<p><strong>Act 1, The glory (2022-23)</strong>: 17 dead ball goals (1st in Ligue 1!), 11 from corners alone, 14.9 xG. OM were slightly overperforming (+2.1 goals vs xG). A 73-point season, their best in 5 years.</p>

<p><strong>Act 2, The collapse (2023-24 and 2024-25)</strong>: a crash to 6 then 7 dead ball goals. But note the 2023-24 xG: <strong>13.0 xG for just 6 goals</strong> (-7.0 underperformance). OM were creating good set piece chances, they just weren’t converting them. It was bad luck as much as inefficiency. De Zerbi’s first season was, from this perspective, quite a feat: despite dreadful set pieces, OM still finished 2nd.</p>

<p><strong>Act 3, The recovery (2025-26)</strong>: 7 dead ball goals, 4th in Ligue 1. Corners are improving (6 goals, 3rd in L1). One could argue the staff’s work (particularly Pancho Abardonado, apparently the set piece coach) has been <a href="https://x.com/TeamOM_Officiel/status/2029306295904272504">unfairly criticized</a>.</p>

<p>But a black hole remains.</p>

<p><img src="/assets/en_marseille_evolution_plot.png" alt="Marseille evolution: DB Goals vs xG" /></p>

<hr />

<h2 id="4-marseilles-black-hole-0-goals-from-indirect-free-kicks">4. Marseille’s black hole: 0 goals from indirect free kicks</h2>

<p>Zero. 21 shots from indirect free kicks this season, not a single goal. An xG per shot of 0.060, the worst among Ligue 1’s top 6. OM aren’t even creating good chances from these situations.</p>

<p><img src="/assets/en_table_top10_ligue1.png" alt="Top 10 Ligue 1, Dead Ball Goals" /></p>

<p>Look at Lorient: 10th in the table, 37 points, yet <strong>level with PSG and Lens</strong> on dead ball goals (10 each). Lorient score 4 from indirect free kicks. Marseille: zero. Even Metz, bottom of Ligue 1 (13 points), have scored 3 non-corner dead ball goals.</p>

<p>The comparison is harsh but raises questions.</p>

<p>To be fair, the coaching staff have clearly improved corners (from 3 goals / 15th in L1 to 6 goals / 3rd). But indirect free kicks (long throw-ins, set plays from wide free kicks) remain an open construction site. The xG confirms it: 0.060 per shot on indirect FKs, versus 0.097 on corners. You might think one or two free kick goals would bring OM back to average. Not even: OM simply aren’t creating good chances from these situations.</p>

<hr />

<h2 id="5-bayern-munich-the-best-attack-in-europe-period">5. Bayern Munich: the best attack in Europe, period</h2>

<p>If you’re looking for a model, it’s Bayern. Not because they’re the best from set pieces (Arsenal and Dortmund have 19 goals vs 14), but because they’re the best <strong>at everything simultaneously</strong>.</p>

<p><img src="/assets/en_bayern_european_context.png" alt="Bayern in European context" /></p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Bayern</th>
      <th>Rank /96</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Points/match</td>
      <td>2.58</td>
      <td><strong>1st</strong></td>
    </tr>
    <tr>
      <td>Goals/match</td>
      <td>3.46</td>
      <td><strong>1st</strong></td>
    </tr>
    <tr>
      <td>Open play goals</td>
      <td>65</td>
      <td><strong>1st</strong></td>
    </tr>
    <tr>
      <td>DB goals/match</td>
      <td>0.54</td>
      <td>2nd</td>
    </tr>
    <tr>
      <td>Corner conversion</td>
      <td>20.0%</td>
      <td>2nd</td>
    </tr>
  </tbody>
</table>

<p>They’re the <strong>only club</strong> positioned in the top right of every scatter plot: elite in open play AND from set pieces. Arsenal are co-leaders on set pieces (19 goals, tied with Dortmund) but relatively modest in open play (1.26 goals/match). Barcelona are strong in open play but average from set pieces. Inter are good at both but a notch below. Bayern have no blind spots.</p>

<p>And unlike Dortmund or Tottenham, this efficiency is backed by xG: 9.8 DB xG for 14 goals, an overperformance of +4.2, high but not outrageous.</p>

<hr />

<h2 id="6-dortmund-a-house-of-cards-built-on-corners">6. Dortmund: a house of cards built on corners?</h2>

<p>Borussia Dortmund co-lead Europe in dead ball goals this season with Arsenal: <strong>19 goals</strong> (13 corners, 6 indirect free kicks). Impressive? Yes. Sustainable? Probably not.</p>

<p><img src="/assets/en_bundesliga_big3_evolution.png" alt="Bundesliga Big 3, 5-season evolution" /></p>

<p>The problem is in the xG: Dortmund have scored 19 goals from just 11.8 xG. That’s an overperformance of +7.2, the second highest in our entire dataset (484 team-seasons). The only comparable season? Union Berlin 2022-23, with +10.5 overperformance on set pieces. The following year, Union Berlin collapsed.</p>

<p><img src="/assets/en_table_top20_all_seasons.png" alt="Top 20 dead ball seasons in Europe" /></p>

<p>But there’s perhaps a warning signal and a “dependency”: <strong>34.5% of Dortmund’s goals come from set pieces</strong>. Just 32 open play goals in 26 matches, their worst in 5 seasons. Set pieces are masking an open play attack that’s sputtering. 8 Bundesliga matchdays remain: the end of the season will tell whether this overperformance holds or corrects, as it has historically for other clubs.</p>

<hr />

<h2 id="7-lens-the-corner-machine-that-broke-down-in-november">7. Lens: the corner machine that broke down in November</h2>

<p>Lens are 2nd in Ligue 1 with 56 points and L1’s corner leader (10 goals). But there’s a before and after November.</p>

<p>On X (formerly Twitter), <a href="https://x.com/Laurentmazure/status/2032907605606080960">@Laurentmazure</a> recently complained:</p>

<blockquote>
  <p><em>“At some point, the media and pundits covering RC Lens need to stop saying the team is ‘effective’ and ‘dangerous’ from set pieces. That was true until November. Since then, Lens have scored just 1 of their last 26 goals from set pieces!”</em></p>
</blockquote>

<p>And <a href="https://x.com/MechTuyot/status/2032909166894092442">@MechTuyot</a> added:</p>

<blockquote>
  <p><em>“That’s the thing… you score two goals in 3 matches from set pieces, and you’re labeled a set piece team for the next 24 months, even if you lose your taker and your aerial threat… regardless of the league or the team.”</em></p>
</blockquote>

<p>The data proves them entirely right. Look at the curve.</p>

<p><img src="/assets/en_lens_timeline.png" alt="Lens, Dead Ball Timeline" /></p>

<p>The cumulative curve is brutal: <strong>9 dead ball goals in 14 matches through November, then just 1 in 12 matches since</strong>. The curve flatlines completely after November 22.</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Matches</th>
      <th>DB Goals</th>
      <th>Conversion</th>
      <th>DB xG</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Aug-Nov</td>
      <td>14</td>
      <td><strong>9</strong></td>
      <td>15.5%</td>
      <td>11.3</td>
    </tr>
    <tr>
      <td>Dec-Mar</td>
      <td>12</td>
      <td><strong>1</strong></td>
      <td>1.9%</td>
      <td>5.7</td>
    </tr>
  </tbody>
</table>

<p>It’s not a volume problem. Lens keep shooting (47 dead ball shots since December). Conversion dropped from 15.5% to 1.9%. The xG also halved. The quality of chances has degraded, not just the finishing. @MechTuyot’s observation is relevant: was a set piece taker or an aerial threat lost? The data doesn’t say who, but it confirms the when (late November) and the magnitude (total collapse).
It will be interesting to see whether Lens recover their efficiency by the end of the season, or whether the drop-off is structural.</p>

<p>The most ironic part: pundits continue praising Lens’ set pieces in March, when they haven’t worked for <strong>four months</strong>. Reputations outlive facts, and that’s precisely why we need data.</p>

<hr />

<h2 id="8-ligue-1-bottom-of-the-class-on-set-pieces">8. Ligue 1, bottom of the class on set pieces</h2>

<p>This isn’t just a Marseille problem or any single club’s issue. It’s the <strong>entire league</strong> that’s behind.</p>

<p><img src="/assets/en_leagues_evolution_5seasons.png" alt="League averages, 5-season evolution" /></p>

<table>
  <thead>
    <tr>
      <th>League</th>
      <th>DB Goals/team</th>
      <th>DB Conv</th>
      <th>xG/DB shot</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Premier League</strong></td>
      <td><strong>8.9</strong></td>
      <td><strong>9.2%</strong></td>
      <td><strong>0.111</strong></td>
    </tr>
    <tr>
      <td>Bundesliga</td>
      <td>7.8</td>
      <td>9.3%</td>
      <td>0.099</td>
    </tr>
    <tr>
      <td>Serie A</td>
      <td>7.8</td>
      <td>8.3%</td>
      <td>0.100</td>
    </tr>
    <tr>
      <td>La Liga</td>
      <td>6.4</td>
      <td>7.3%</td>
      <td>0.096</td>
    </tr>
    <tr>
      <td><strong>Ligue 1</strong></td>
      <td><strong>5.3</strong></td>
      <td><strong>6.7%</strong></td>
      <td><strong>0.093</strong></td>
    </tr>
  </tbody>
</table>

<p>Ligue 1 is <strong>last on every metric</strong>: fewer goals, worse conversion, lower shot quality (xG/shot). And the gap is widening: in 2021-22, L1 averaged 9.8 DB goals per team, ahead of the Premier League (9.2). By 2025-26, it’s 5.3 versus 8.9. The decline is spectacular.</p>

<p>Is it a tactical issue? Corner-taking quality? Defensive quality? The physique of attackers/defenders in aerial duels? Probably a bit of everything. But the data is clear: for equal investment in set pieces, a Ligue 1 club will get significantly less return than a Premier League club.</p>

<hr />

<h2 id="9-dead-ball-conversion-predicts-nothing-for-top-teams">9. Dead ball conversion predicts nothing for top teams</h2>

<p>This may be the most counter-intuitive finding of the entire analysis. You’d think the teams that convert best from set pieces are the ones at the top of the table. That’s true… on average. But look closer, and it’s far more nuanced.</p>

<p><img src="/assets/en_nonlinear_analysis.png" alt="Non-linear analysis" /></p>

<p>I split the 484 team-seasons into performance quartiles (by points, normalized within each league-season). Here are the correlations between dead ball conversion and points <strong>within each tier</strong>:</p>

<table>
  <thead>
    <tr>
      <th>Tier</th>
      <th>DB Conv vs Points (r)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bottom 25%</td>
      <td>+0.12</td>
    </tr>
    <tr>
      <td>25-50%</td>
      <td>+0.05</td>
    </tr>
    <tr>
      <td>50-75%</td>
      <td>+0.12</td>
    </tr>
    <tr>
      <td><strong>Top 25%</strong></td>
      <td><strong>-0.11</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>For teams in the top 25%, dead ball conversion is slightly negatively correlated with points.</strong> In other words, among good teams, converting more from set pieces doesn’t help you climb the table. It can even signal over-dependency (hello Tottenham).</p>

<p>The examples speak for themselves:</p>
<ul>
  <li>Juventus 2024-25: 2.7% DB conversion, 70 points (4th in Serie A)</li>
  <li>Tottenham 2025-26: 21.5% DB conversion, 30 points (16th in PL)</li>
  <li>Marseille 2024-25: 6.8% DB conversion, 65 points (2nd in Ligue 1)</li>
</ul>

<p>The rolling correlation (bottom right chart) shows the effect of DB conversion is <strong>strongest between 30 and 50 points</strong>, i.e. mid-table teams. For them, set pieces can make the difference between survival and relegation. But above 65 points, the effect vanishes entirely.</p>

<p>Between groups, the gap is massive: the top 10% DB converters average <strong>16.8 more points</strong> than the bottom 10% (p&lt;0.001). But within a given performance group, DB conversion explains nothing.</p>

<hr />

<h2 id="10-the-marseille-paradox-what-if-improving-set-pieces-changes-nothing">10. The Marseille paradox: what if improving set pieces changes nothing?</h2>

<p>Let’s summarize OM’s situation. Overall conversion of 14.2% (excellent, well above average). Dead ball conversion of 7.6% (poor). Zero goals from indirect free kicks. The diagnosis is clear: a gap in the armor.</p>

<p>But the data also says that for a team of Marseille’s caliber (top 25% in Ligue 1), <strong>improving dead ball conversion doesn’t mechanically translate to more points</strong>. OM finished 2nd in 2024-25 with one of the worst corner records in L1’s top 15. They’re 3rd in 2025-26 with significantly better efficiency. The table hasn’t shifted proportionally.</p>

<p>If OM scored 3-4 more goals from indirect free kicks this season (which would simply be the Ligue 1 average), it might mean 2-3 extra points. Match scenarios could change too. But is that what closes an 8-point gap to PSG and Lens? Probably not.</p>

<p>The real levers are open play quality, defensive solidity, and not overperforming xPoints by 5 points, which tends to correct over time.</p>

<p>And that’s exactly what Habib Beye says, <a href="https://x.com/MassiliaZone/status/2032092476702670859">relayed by @MassiliaZone</a>:</p>

<blockquote>
  <p><em>“I value hard work, but situations need to open up beyond just set pieces.”</em></p>
</blockquote>

<p>Marseille’s coach reads it the same way as the data. Set pieces are worth working on (and the staff probably don’t do it enough). But Marseille’s ceiling doesn’t depend on it. Beye knows it, the xG confirms it.</p>

<p><strong>OM should invest more in set pieces, especially free kicks, but the return on investment is likely to be moderate. It’s not the only lever that changes Marseille’s ceiling.</strong></p>

<hr />

<h2 id="in-summary">In summary</h2>

<p>Set pieces are a bonus, not an engine. Tottenham are on Europe’s corner podium (14 goals, 3rd) and are fighting relegation. Marseille were among Ligue 1’s worst from corners in 2024-25 and finished 2nd. Data from 484 team-seasons confirms it: for good teams, dead ball conversion predicts nothing.</p>

<p>What sets Arsenal or Bayern apart isn’t just being good from set pieces. It’s being good at everything. Set pieces are merely the icing on a cake that needs to exist first. Lens have been ineffective from set pieces since November, but are a deserving and impressive 2nd.</p>

<p>Of course, the 2025-26 sample remains fragile (26-30 matchdays), and some trends (Dortmund’s overperformance, Lens’ collapse, Tottenham’s conversion) could correct by season’s end. The four previous seasons are complete and robust. That’s the whole point of cross-referencing analyses.</p>

<p>You can talk about football with data without betraying the game. Set pieces are a perfect subject: technical enough for numbers to add value, visible enough for everyone to have an opinion. And often, data confirms the intuition of supporters and coaches. For instance, I was convinced Marseille were catastrophic on set pieces. It’s a bit more subtle than I thought. Sometimes, data refines our views.</p>

<p><strong>Data is a starting point, a clue, food for thought, not a final verdict. It doesn’t tell everything, but sometimes pushes you to change or refine a snap judgement. With the democratization of tools and data, we can hope for football debates that are a little less instinct-driven and a little more evidence-based. And why not, more compelling.</strong></p>

<hr />

<p><em>Data and source code for this analysis are available on GitHub. All visualizations were generated with Python (matplotlib, soccerdata, scipy). xG data comes from Understat. Monte Carlo simulations for xPoints use 5,000 iterations per match with a Poisson distribution.</em></p>

<p><em>The 2025-26 season is not over: current figures reflect ~26-30 matchdays depending on the league. The trends identified are robust, but final standings and some overperformances (Dortmund, Tottenham) may shift. I’ll publish an update at the end of the season to see what held up and what corrected.</em></p>

<p><em>Have an opinion, a question, or want to share this article? Want to know how I did it? Contact me at mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026cornersfootball</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{Corners, Free Kicks, and Set Pieces Across Europe's Top Football Leagues: What the Data Actually Says}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CornersSetPiecesFootballEN/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CornersSetPiecesFootballEN/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="football" /><category term="data analysis" /><category term="corners" /><category term="set pieces" /><category term="Ligue 1" /><category term="Premier League" /><category term="Marseille" /><category term="Arsenal" /><category term="xG" /><category term="Understat" /><summary type="html"><![CDATA[Corners and set pieces are one of football’s most debated topics. “Arsenal are unstoppable from corners,” “Marseille are hopeless on free kicks,” and so on. Opinions, gut feelings, conventional wisdom. But sometimes the data surprises: did you know that Tottenham, fighting relegation in the Premier League, are on the European podium for corner goals this season, just behind Arsenal and Inter? What if we actually checked? I analyzed 5 seasons of data across Europe’s top 5 leagues (Premier League, Ligue 1, Serie A, Bundesliga, La Liga). Every shot, every goal, every expected goal (xG) from corners, indirect free kicks, and direct free kicks. 484 team-seasons. Here’s what I found in 10 key takeaways, and some results are surprising, underappreciated, or counter-intuitive.]]></summary></entry><entry><title type="html">Corners, coups francs, fantasmes et surprises dans les grands championnats européens de football : ce que les données disent vraiment</title><link href="http://blog.mathieuacher.com/CornersSetPiecesFootball/" rel="alternate" type="text/html" title="Corners, coups francs, fantasmes et surprises dans les grands championnats européens de football : ce que les données disent vraiment" /><published>2026-03-18T10:00:00+00:00</published><updated>2026-03-18T10:00:00+00:00</updated><id>http://blog.mathieuacher.com/CornersSetPiecesFootball</id><content type="html" xml:base="http://blog.mathieuacher.com/CornersSetPiecesFootball/"><![CDATA[<p>On parle souvent des corners et des coups de pied arrêtés (CPA) au café du commerce, sur les réseaux sociaux, ou dans les médias. “Arsenal est injouable sur corners”, “L’OM est nul sur coups francs”, etc. Des impressions, des ressentis. Et parfois des surprises : saviez-vous que Tottenham, à la lutte pour le maintien en Premier League, est sur le podium européen des buts sur corners cette saison, juste derrière Arsenal et l’Inter ? Non ? Oui ? 
Et si on allait vérifier ?
J’ai analysé <strong>5 saisons de données</strong> sur les 5 grands championnats européens (Premier League, Ligue 1, Serie A, Bundesliga, La Liga). Chaque tir, chaque but, chaque expected goal (xG) sur corner, coup franc indirect, coup franc direct. 484 équipes-saisons. Voici ce que j’ai trouvé en 10 points clés, et certains résultats me semblent surprenants, méconnus, ou contre-intuitifs.</p>

<p><em>Données : Understat via understatapi. 5 ligues européennes, 5 saisons (2021-2026), 484 équipes-saisons. Note : la saison 2025-26 est en cours (~26-30 journées selon les championnats). Les tendances sont claires mais les chiffres évolueront. Je mettrai à jour en fin de saison.</em></p>

<h2 id="1-tottenham-roi-des-corners-et-16e-de-premier-league">1. Tottenham, roi des corners… et 16e de Premier League</h2>

<p>C’est l’histoire la plus folle de cette saison (en cours, ~30 journées de PL jouées). Tottenham est <strong>3e européen</strong> sur les buts marqués sur corners : 14 buts, derrière Arsenal (16) et l’Inter (15). Conversion de 21.5%, la meilleure d’Europe.</p>

<p>Le problème ? Les Spurs sont actuellement <strong>16es de Premier League</strong> avec 30 points.</p>

<p><img src="/assets/fr_tottenham_open_vs_corners.png" alt="Tottenham vs Arsenal, Jeu ouvert vs Corners" /></p>

<p>Le graphique est sans appel. Tottenham est isolé en haut à gauche : beaucoup de buts sur corners, très peu en jeu ouvert (25 buts, 14e du championnat). Arsenal, lui, est en haut à droite : 16 buts sur corners ET 39 en jeu ouvert.</p>

<p><strong>35.0% des buts de Tottenham viennent de phases arrêtées.</strong> C’est le taux le plus élevé parmi les clubs des grands championnats (hors relégables). Quand plus d’un tiers de tes buts dépendent de situations de jeu qui représentent ~15% des tirs, tu construis sur du sable.</p>

<p>À titre de comparaison, le Bayern Munich, meilleure attaque d’Europe, n’est qu’à 15.6% de dépendance aux CPA. Parce que quand tu marques 65 buts en jeu ouvert, les corners sont un bonus, pas une béquille.</p>

<p>Et puis il y a Arsenal, l’ovni : <strong>16 buts sur corners (1er en Europe)</strong>, mais également 39 en jeu ouvert, 70 points (1er de Premier League). Arsenal ne se contente pas d’être redoutable offensivement sur CPA, ils le sont aussi dans le jeu. Lors du match de Ligue des Champions contre le Bayer Leverkusen, <a href="https://x.com/bayer04_en/status/2031723079886401891">le compte officiel du Bayer 04</a> a veinement interdit à Arsenal d’obtenir des corners. Des voies s’élèvent même en Premier League. 
Dans une interview explosive, John Obi Mikel a carrément accusé Arsenal de tricher ou que le succès d’Arsenal dépend uniquement des corners: https://talksport.com/football/4043546/john-obi-mikel-interview-arsenal-chelsea-mourinho-nigeria/. 
Tottenham est un parfait contre-exemple : il n’y a pas que les corners… 
Et on peut aussi penser que Tottenham est en grand danger, car sans les CPA la situation serait encore plus compliquée que la 16ème place.</p>

<hr />

<h2 id="2-elche-2022-23--107-tirs-sur-corners-lanatomie-du-désespoir">2. Elche 2022-23 : 107 tirs sur corners, l’anatomie du désespoir</h2>

<p>Outre Tottenham en 2025-26, il y a d’autres cas extrêmes : Elche, saison 2022-23, La Liga. L’équipe a été reléguée avec 25 points, et pourtant, elle a tiré <strong>107 fois sur corners</strong>, le record de La Liga cette saison-là.</p>

<p><img src="/assets/fr_elche_corners_2022_23.png" alt="Distribution des tirs sur corners, La Liga 2022-23" /></p>

<p>Le z-score est de 2.26, un outlier statistique. Mais le graphique de droite est encore plus parlant : <strong>26.4% de tous les tirs d’Elche venaient de corners</strong>. La moyenne de La Liga est à 15.6%. Personne d’autre ne dépasse 19%.</p>

<p>Pourquoi ? Parce qu’Elche ne savait rien faire d’autre.
Seulement 406 tirs au total (le plus bas de La Liga), une création en jeu ouvert inexistante. Les corners étaient leur seule route vers le but. Lucas Boyé, leur attaquant de pointe, a tiré 19 fois sur corner à lui seul, un homme-cible qui faisait tout le boulot aérien.</p>

<p>Quand tu ne sais rien faire d’autre, il te reste les corners. Tottenham 2025-26 est une version moins extrême du même syndrome.</p>

<p>Rassurez-vous, je n’ai pas regardé tous les matchs de Elche. En fait, je pense que j’ai dû en regarder à peu près zéro. Mais les statistiques sont assez claires (j’ai regardé également si c’était une anomalie des données ou par exemple un match mal renseigné peut explique ce haut score en corners, mais non, a priori). Ceci dit, <strong>je cherche des suiveurs ou mieux supporters de Elche, car c’est un phénomène assez incroyable</strong>, et malgré quelques recherches ici et là, je n’ai rien trouvé qui relate cet exploit de Elche.</p>

<hr />

<h2 id="3-lom-de-la-honte-à-la-renaissance-partielle-sur-corners">3. L’OM, de la honte à la renaissance (partielle) sur corners</h2>

<p>En novembre 2025, <a href="https://x.com/Rubenzf911/status/1985468834401116253">@Rubenzf911</a> posait la question que beaucoup de supporters marseillais avaient en tête :</p>

<blockquote>
  <p><em>“Les CPA de l’OM est-ce qu’un jour on va tirer la sonnette d’alarme svp?”</em></p>
</blockquote>

<p>La sonnette, les données l’ont tirée depuis un moment. Voici l’évolution de Marseille sur 5 saisons :</p>

<p><img src="/assets/fr_table_marseille_evolution.png" alt="Évolution Marseille, 5 saisons" /></p>

<p>Le tableau raconte une histoire en trois actes :</p>

<p><strong>Acte 1, La gloire (2022-23)</strong> : 17 buts sur CPA (1er de Ligue 1 !), 11 sur corners seuls, 14.9 xG. L’OM surperformait légèrement (+2.1 buts vs xG). Saison à 73 points, la meilleure des 5 années.</p>

<p><strong>Acte 2, La chute (2023-24 et 2024-25)</strong> : effondrement à 6 puis 7 buts CPA. Mais attention au xG de 2023-24 : <strong>13.0 xG pour seulement 6 buts</strong> (-7.0 de sous-performance). L’OM créait de bonnes occasions sur CPA, il ne les convertissait juste pas. C’était de la malchance autant que de l’inefficacité. La première saison de De Zerbi est d’un certain point de vue une sacré performance : malgré des CPA inefficaces l’OM a quand même fini 2ème.</p>

<p><strong>Acte 3, La remontée (2025-26)</strong> : 7 buts CPA, 4e de Ligue 1. Les corners vont mieux (6 buts, 3e en L1). On peut donc se dire que le travail du staff (avec en ligne de mire Pancho Abardonado, apparemment entraîneur en charge des CPA) est inutilement critiqué https://x.com/TeamOM_Officiel/status/2029306295904272504</p>

<p>Mais un trou noir persiste.</p>

<p><img src="/assets/fr_marseille_evolution_plot.png" alt="Évolution Marseille, Buts CPA vs xG" /></p>

<hr />

<h2 id="4-le-trou-noir-marseillais--0-but-sur-coups-francs-indirects">4. Le trou noir marseillais : 0 but sur coups francs indirects</h2>

<p>Zéro. 21 tirs issus de coups francs indirects cette saison, pas un seul but. Un xG par tir de 0.060, le pire des 6 premiers de Ligue 1. L’OM ne crée même pas de bonnes occasions sur ces phases de jeu.</p>

<p><img src="/assets/fr_table_top10_ligue1.png" alt="Top 10 Ligue 1, Phases arrêtées" /></p>

<p>Regardons Lorient : 10e au classement, 37 points, et pourtant à égalité avec le PSG et Lens sur les buts CPA (10 chacun). Lorient marque 4 buts sur coups francs indirects. L’OM : zéro. Même Metz, dernier de Ligue 1 (13 points), a marqué 3 buts sur CPA hors corners.</p>

<p>La comparaison est cruelle mais pose des questions.</p>

<p>Soyons justes : le travail du staff a clairement amélioré les corners (de 3 buts / 15e en L1 à 6 buts / 3e). Mais les coups francs indirects (rentrées en touche longues, combinaisons sur coups francs excentrés) restent un chantier ouvert. Les xG le confirment : 0.060 par tir sur CF indirects, contre 0.097 sur corners. 
Il suffirait d’un ou deux buts sur coup franc pour remettre l’OM à niveau me direz-vous… Même pas : L’OM ne crée tout simplement pas de bonnes occasions sur ces phases-là.</p>

<hr />

<h2 id="5-bayern-munich--la-meilleure-attaque-deurope-point-final">5. Bayern Munich : la meilleure attaque d’Europe, point final</h2>

<p>Si vous cherchez un modèle, c’est le Bayern. Pas parce qu’ils sont les meilleurs sur CPA (Arsenal et Dortmund font 19 buts CPA contre 14), mais parce qu’ils sont les meilleurs <strong>partout en même temps</strong>.</p>

<p><img src="/assets/fr_bayern_contexte_europeen.png" alt="Bayern en contexte européen" /></p>

<table>
  <thead>
    <tr>
      <th>Métrique</th>
      <th>Bayern</th>
      <th>Rang /96</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Points/match</td>
      <td>2.58</td>
      <td><strong>1er</strong></td>
    </tr>
    <tr>
      <td>Buts/match</td>
      <td>3.46</td>
      <td><strong>1er</strong></td>
    </tr>
    <tr>
      <td>Buts en jeu ouvert</td>
      <td>65</td>
      <td><strong>1er</strong></td>
    </tr>
    <tr>
      <td>Buts CPA/match</td>
      <td>0.54</td>
      <td>2e</td>
    </tr>
    <tr>
      <td>Conversion corners</td>
      <td>20.0%</td>
      <td>2e</td>
    </tr>
  </tbody>
</table>

<p>C’est le <strong>seul club</strong> situé en haut à droite de tous les nuages de points : élite en jeu ouvert ET en phases arrêtées. Arsenal est co-leader sur CPA (19 buts, à égalité avec Dortmund) mais relativement modeste en jeu ouvert (1.26 but/match). Barcelone est fort en jeu ouvert mais moyen sur CPA. Inter est bon partout mais à un cran en dessous. Le Bayern n’a aucun angle mort.</p>

<p>Et contrairement à Dortmund ou Tottenham, cette efficacité est soutenue par les xG : 9.8 xG CPA pour 14 buts, une surperformance de +4.2, élevée mais pas délirante.</p>

<hr />

<h2 id="6-dortmund--un-château-de-cartes-construit-sur-corners">6. Dortmund : un château de cartes construit sur corners?</h2>

<p>Borussia Dortmund est co-leader européen des buts sur phases arrêtées cette saison avec Arsenal : <strong>19 buts</strong> (13 corners, 6 coups francs indirects). Impressionnant ? Oui. Durable ? Probablement pas.</p>

<p><img src="/assets/fr_bundesliga_big3_evolution.png" alt="Bundesliga Big 3, Évolution 5 saisons" /></p>

<p>Le problème est dans les xG : Dortmund a marqué 19 buts pour seulement 11.8 xG. C’est une surperformance de +7.2, la deuxième plus élevée de tout notre dataset (484 équipes-saisons). La seule saison comparable ? Union Berlin 2022-23, avec +10.5 de surperformance sur CPA. L’année suivante, Union Berlin s’est écroulé.</p>

<p><img src="/assets/fr_table_top20_all_seasons.png" alt="Top 20 meilleures saisons CPA en Europe" /></p>

<p>Mais il y a peut-être un signal d’alarme et une “dépendance” : <strong>34.5% des buts de Dortmund viennent de CPA</strong>. Seulement 32 buts en jeu ouvert en 26 matchs, le pire de leur 5 dernières saisons. Les phases arrêtées masquent une attaque en jeu ouvert qui tourne au ralenti. Il reste 8 journées en Bundesliga : la fin de saison dira si cette surperformance tient ou si elle se corrige, comme elle l’a fait historiquement pour d’autres clubs.</p>

<hr />

<h2 id="7-lens--la-machine-à-corners-qui-sest-enrayée-en-novembre">7. Lens : la machine à corners qui s’est enrayée en novembre</h2>

<p>Lens est 2e de Ligue 1 avec 56 points et leader sur les corners en L1 (10 buts). Mais il y a un avant et un après novembre.</p>

<p>Sur X (ex-Twitter), <a href="https://x.com/Laurentmazure/status/2032907605606080960">@Laurentmazure</a> s’agaçait récemment :</p>

<blockquote>
  <p><em>“Il faudra aussi, un jour, que les médias et ceux qui commentent les matchs du #RCLens (cc Josse-Bravo) arrêtent de dire que le Racing est “efficace” et toujours “dangereux” sur CPA. C’était le cas jusqu’en novembre. Depuis, Lens n’a marqué que 1 de ses 26 derniers buts sur CPA !”</em></p>
</blockquote>

<p>Et <a href="https://x.com/MechTuyot/status/2032909166894092442">@MechTuyot</a> de renchérir :</p>

<blockquote>
  <p><em>“Ah ça… tu marques deux buts en 3 matchs sur cpa, t’es catalogué équipe de cpa pour les 24 mois qui suivent même si tu perds ton tireur et ton mec qui met des têtes… peu importe le championnat et l’équipe.”</em></p>
</blockquote>

<p>Les données leur donnent entièrement raison. Regardez la courbe.</p>

<p><img src="/assets/fr_lens_timeline.png" alt="Lens, Timeline CPA" /></p>

<p>La courbe cumulative est brutale : <strong>9 buts CPA en 14 matchs jusqu’en novembre, puis 1 seul but en 12 matchs depuis</strong>. La courbe se fige complètement après le 22 novembre.</p>

<table>
  <thead>
    <tr>
      <th>Période</th>
      <th>Matchs</th>
      <th>Buts CPA</th>
      <th>Conversion</th>
      <th>xG CPA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Août-Novembre</td>
      <td>14</td>
      <td><strong>9</strong></td>
      <td>15.5%</td>
      <td>11.3</td>
    </tr>
    <tr>
      <td>Décembre-Mars</td>
      <td>12</td>
      <td><strong>1</strong></td>
      <td>1.9%</td>
      <td>5.7</td>
    </tr>
  </tbody>
</table>

<p>Ce n’est pas un problème de volume. Lens continue de tirer (47 tirs CPA depuis décembre). La conversion est passée de 15.5% à 1.9%. Le xG aussi a chuté de moitié. La qualité des occasions s’est dégradée, pas juste la finition. L’observation de @MechTuyot est pertinente : est-ce qu’un tireur ou un “mec qui met des têtes” a été perdu ? Les données ne disent pas qui, mais elles confirment le quand (fin novembre) et l’ampleur (effondrement total). 
Il sera intéressant de voir si Lens retrouve son efficacité d’ici la fin de saison, ou si le décrochage est structurel.h</p>

<p>Le plus ironique : les commentateurs continuent de vanter les CPA lensoises en mars, alors que ça fait <strong>quatre mois</strong> que ça ne fonctionne plus. Les réputations survivent longtemps aux faits, et c’est exactement pour ça qu’on a besoin de données. 
<strong>Message de service: les commentaires/analystes/émissions foot, qui cumulent des milliers d’heures, gagneraient à s’appuyer sur de telles données pour alimenter les débats.</strong> C’est en tout cas un point de départ intéressant pour de nombreux débats.</p>

<hr />

<h2 id="8-la-ligue-1-dernière-de-la-classe-sur-phases-arrêtées">8. La Ligue 1, dernière de la classe sur phases arrêtées</h2>

<p>Ce n’est pas juste un problème de Marseille ou de tel club. C’est tout le championnat qui est en retard.</p>

<p><img src="/assets/fr_ligues_evolution_5saisons.png" alt="Moyennes par ligue, Évolution 5 saisons" /></p>

<table>
  <thead>
    <tr>
      <th>Ligue</th>
      <th>Buts CPA/équipe</th>
      <th>Conv. CPA</th>
      <th>xG/tir CPA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Premier League</strong></td>
      <td><strong>8.9</strong></td>
      <td><strong>9.2%</strong></td>
      <td><strong>0.111</strong></td>
    </tr>
    <tr>
      <td>Bundesliga</td>
      <td>7.8</td>
      <td>9.3%</td>
      <td>0.099</td>
    </tr>
    <tr>
      <td>Serie A</td>
      <td>7.8</td>
      <td>8.3%</td>
      <td>0.100</td>
    </tr>
    <tr>
      <td>La Liga</td>
      <td>6.4</td>
      <td>7.3%</td>
      <td>0.096</td>
    </tr>
    <tr>
      <td><strong>Ligue 1</strong></td>
      <td><strong>5.3</strong></td>
      <td><strong>6.7%</strong></td>
      <td><strong>0.093</strong></td>
    </tr>
  </tbody>
</table>

<p>La Ligue 1 est <strong>dernière sur tout</strong> : moins de buts, moins bonne conversion, moins bonne qualité d’occasion (xG/tir). Et l’écart se creuse : en 2021-22, la L1 était à 9.8 buts CPA par équipe, devant la Premier League (9.2). En 2025-26, elle est à 5.3 contre 8.9. Le décrochage est spectaculaire.</p>

<p>Est-ce un problème tactique ? De qualité des tireurs de corners ? De qualité des défenses ? De gabarit des défenseurs/attaquants sur les phases aériennes ? Probablement un peu de tout. Mais les données sont claires : à investissement égal dans les CPA, un club de Ligue 1 en tirera significativement moins qu’un club de Premier League.</p>

<hr />

<h2 id="9-la-conversion-sur-cpa-ne-prédit-rien-pour-les-grosses-équipes">9. La conversion sur CPA ne prédit rien pour les grosses équipes</h2>

<p>C’est peut-être le résultat le plus contre-intuitif de toute l’analyse. On pourrait penser que les équipes qui convertissent le mieux sur CPA sont les meilleures au classement. C’est vrai… en moyenne. Mais en regardant de plus près, c’est beaucoup plus nuancé.</p>

<p><img src="/assets/fr_analyse_nonlineaire.png" alt="Analyse non-linéaire" /></p>

<p>J’ai découpé les 484 équipes-saisons en quartiles de performance (par points, normalisés au sein de chaque ligue-saison). Voici les corrélations entre conversion CPA et points <strong>au sein de chaque tier</strong> :</p>

<table>
  <thead>
    <tr>
      <th>Tier</th>
      <th>Conv. CPA vs Points (r)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bas 25%</td>
      <td>+0.12</td>
    </tr>
    <tr>
      <td>25-50%</td>
      <td>+0.05</td>
    </tr>
    <tr>
      <td>50-75%</td>
      <td>+0.12</td>
    </tr>
    <tr>
      <td><strong>Haut 25%</strong></td>
      <td><strong>-0.11</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>Pour les équipes du top 25%, la conversion CPA est légèrement négativement corrélée avec les points.</strong> Autrement dit, parmi les bonnes équipes, mieux convertir les CPA ne te fait pas monter au classement. Ça peut même être le signe d’une sur-dépendance (hello Tottenham).</p>

<p>Les exemples sont parlants :</p>
<ul>
  <li>Juventus 2024-25 : 2.7% de conversion CPA, 70 points (4e de Serie A)</li>
  <li>Tottenham 2025-26 : 21.5% de conversion CPA, 30 points (16e de PL)</li>
  <li>Marseille 2024-25 : 6.8% de conversion CPA, 65 points (2e de Ligue 1)</li>
</ul>

<p>La corrélation glissante (graphique en bas à droite) montre que l’effet de la conversion CPA est maximal entre 30 et 50 points :les équipes de milieu de tableau. Pour elles, les phases arrêtées peuvent faire la différence entre le maintien et la relégation. Mais au-delà de 65 points, l’effet disparaît complètement.</p>

<p>Entre les groupes, l’écart est massif : les 10% meilleurs convertisseurs CPA ont en moyenne 16.8 points de plus que les 10% pires (p&lt;0.001). Mais à l’intérieur d’un même groupe de niveau, la conversion CPA n’explique rien.</p>

<hr />

<h2 id="10-le-paradoxe-marseillais--et-si-améliorer-les-cpa-ne-changeait-rien-">10. Le paradoxe marseillais : et si améliorer les CPA ne changeait rien ?</h2>

<p>Résumons la situation de l’OM. Conversion globale de 14.2% (excellente, bien au-dessus de la moyenne). Conversion CPA de 7.6% (médiocre). Zéro but sur coups francs indirects. Le diagnostic est clair : un trou dans la raquette.</p>

<p>Mais les données disent aussi que pour une équipe du calibre de Marseille (top 25% de Ligue 1), <strong>améliorer la conversion CPA ne se traduit pas mécaniquement en points supplémentaires</strong>. L’OM est 2e en 2024-25 avec la pire efficacité corners du top 15. Il est 3e en 2025-26 avec une efficacité nettement meilleure. Le classement n’a pas bougé proportionnellement.</p>

<p>Si l’OM marquait 3-4 buts de plus sur coups francs indirects cette saison (ce qui serait simplement la moyenne de Ligue 1), cela ferait peut-être 2-3 points de plus.
Des scénarios de match peuvent changer également. Mais est-ce que c’est ça qui comble un écart de 8 points avec le PSG et Lens ? Probablement pas.</p>

<p>Les autres (vraies) leviers, c’est le jeu ouvert, la solidité défensive, et arrêter de surperformer ses xPoints de 5 points, ce qui finit souvent par se corriger.</p>

<p>Et c’est exactement ce que dit Habib Beye, <a href="https://x.com/MassiliaZone/status/2032092476702670859">relayé par @MassiliaZone</a> :</p>

<blockquote>
  <p><em>“J’accorde beaucoup d’importance au travail mais les situations doivent s’ouvrir aussi sur autre chose que des coups de pieds arrêtés.”</em></p>
</blockquote>

<p>L’entraîneur de l’OM a la même lecture que les données. Les CPA, c’est bien, il faut y travailler (et le staff ne le fait certainement pas assez). Mais le plafond de Marseille ne se joue pas forcément là. Beye le sait, les xG le confirment.</p>

<p><strong>L’OM doit d’avantage investir dans les CPA, surtout les coups francs, mais le “retour sur investissement” risque d’être modéré. Ce n’est pas le seul levier qui change le plafond de Marseille.</strong></p>

<hr />

<h2 id="en-résumé">En résumé</h2>

<p>Les CPA sont un bonus, pas un moteur. Tottenham est sur le podium européen des corners (14 buts, 3e) et lutte pour le maintien. L’OM était un des pires de Ligue 1 sur corners en 2024-25 et a fini 2e. Les données de 484 équipes-saisons le confirment : pour les bonnes équipes, la conversion CPA ne prédit rien.</p>

<p>Ce qui distingue Arsenal ou le Bayern, ce n’est pas uniquement d’être bons sur CPA. C’est d’être bons partout. Les phases arrêtées ne sont qu’une cerise sur un gâteau qui doit d’abord exister. Lens est inefficace depuis novembre sur CPA, mais est un très beau et méritant 2ème.</p>

<p>Bien sûr, sur 2025-26, l’échantillon reste fragile (26-30 journées), et certaines tendances (la surperformance de Dortmund, l’effondrement de Lens, la conversion de Tottenham) pourraient se corriger d’ici la fin de saison. Les quatre saisons précédentes, elles, sont complètes et solides. C’est tout l’intérêt de croiser les analyses.</p>

<p>On peut parler de football avec des données sans trahir le jeu. Les CPA sont un sujet parfait : assez technique pour que les chiffres apportent quelque chose, assez visible pour que tout le monde ait un avis. Et souvent, les données confirment l’intuition des supporters et des coachs ou des “experts”. A titre d’exemple, j’étais persuadé que l’OM était catastrophique sur CPA. C’est un peu plus subtile que ce que je pensais. Aussi, parfois, les données corrigent les avis.</p>

<p><strong>Les données sont un point de départ, un indice, du grain à moudre, pas un jugement final. Elles ne disent pas tout, mais poussent parfois à changer ou raffiner son jugement initial ou un avis tranché. Avec la démocratisation des outils et des données, on peut espérer des débats foot un peu moins à l’instinct et un peu plus étayés. Et pourquoi pas, plus passionnants.</strong></p>

<hr />

<p><em>Les données et le code source de cette analyse sont disponibles sur GitHub. Toutes les visualisations ont été générées avec Python (matplotlib, soccerdata, scipy). Les xG proviennent d’Understat. Les simulations Monte Carlo pour les xPoints utilisent 5000 itérations par match avec distribution de Poisson.</em></p>

<p><em>La saison 2025-26 n’est pas terminée : les chiffres actuels reflètent ~26-30 journées selon les championnats. Les tendances identifiées sont robustes, mais les classements finaux et certaines surperformances (Dortmund, Tottenham) pourraient évoluer. Je publierai une mise à jour en fin de saison pour voir ce qui a tenu et ce qui s’est corrigé.</em></p>

<p><em>Vous avez un avis, une question, envie de partager l’article sur d’autres medium ? Ou simplement savoir comment je m’y suis pris ? Contactez-moi mathieu.acher@irisa.fr</em></p>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026cornersfootballfr</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{Corners, coups francs, fantasmes et surprises dans les grands championnats européens de football : ce que les données disent vraiment}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CornersSetPiecesFootball/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CornersSetPiecesFootball/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="football" /><category term="data analysis" /><category term="corners" /><category term="set pieces" /><category term="Ligue 1" /><category term="Premier League" /><category term="Marseille" /><category term="Arsenal" /><category term="xG" /><category term="Understat" /><summary type="html"><![CDATA[On parle souvent des corners et des coups de pied arrêtés (CPA) au café du commerce, sur les réseaux sociaux, ou dans les médias. “Arsenal est injouable sur corners”, “L’OM est nul sur coups francs”, etc. Des impressions, des ressentis. Et parfois des surprises : saviez-vous que Tottenham, à la lutte pour le maintien en Premier League, est sur le podium européen des buts sur corners cette saison, juste derrière Arsenal et l’Inter ? Non ? Oui ? Et si on allait vérifier ? J’ai analysé 5 saisons de données sur les 5 grands championnats européens (Premier League, Ligue 1, Serie A, Bundesliga, La Liga). Chaque tir, chaque but, chaque expected goal (xG) sur corner, coup franc indirect, coup franc direct. 484 équipes-saisons. Voici ce que j’ai trouvé en 10 points clés, et certains résultats me semblent surprenants, méconnus, ou contre-intuitifs.]]></summary></entry><entry><title type="html">Can Coding Agents Program in M&amp;amp;Ms Language?</title><link href="http://blog.mathieuacher.com/CodingAgentsMnMLang/" rel="alternate" type="text/html" title="Can Coding Agents Program in M&amp;amp;Ms Language?" /><published>2026-03-17T13:00:00+00:00</published><updated>2026-03-17T13:00:00+00:00</updated><id>http://blog.mathieuacher.com/CodingAgentsMnMLang</id><content type="html" xml:base="http://blog.mathieuacher.com/CodingAgentsMnMLang/"><![CDATA[<p>What happens when you challenge a coding agent with an esoteric programming language it has never seen (freshly invented, with very few examples), and where the source code is literally colored M&amp;Ms on a table? No documentation beyond the opcode table. No Stack Overflow threads. No training examples to memorize. Just six candy colors, a stack machine, and a dare.
<img src="/assets/mnm-mul-table.png" alt="Multiplication table in MNM Lang, written by Claude Code and rendered as M&amp;M candies" style="max-height: 400px;" />
I tried it with <a href="https://github.com/mufeedvh/mnmlang">MNM Lang</a>, a toy language amazingly created by <a href="https://mufeedvh.com/posts/i-made-a-programming-language-with-mnms/">Mufeed VH</a> where programs are grids of candy-colored tokens (<code class="language-plaintext highlighter-rouge">B</code>lue, <code class="language-plaintext highlighter-rouge">G</code>reen, <code class="language-plaintext highlighter-rouge">R</code>ed, <code class="language-plaintext highlighter-rouge">Y</code>ellow, <code class="language-plaintext highlighter-rouge">O</code>range, brow<code class="language-plaintext highlighter-rouge">N</code>). Token length encodes operand values. Strings and inputs live in a sidecar JSON file. The whole thing compiles to a PNG of M&amp;M sprites. So funny by design, for humans. But could an LLM learn to write it?</p>

<p>Again: no training examples, no worked solutions, no hints. Just the README spec and the opcode table. Could an LLM figure out the semantics and produce correct MNM Lang programs?</p>

<p>I gave <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> (Claude Opus 4.6) 26 challenges of increasing difficulty, from “sum 1 to N” to “write a Brainfuck interpreter”. No hand-holding, no worked examples beyond the opcode table in the README. The agent had to figure out the language semantics from the spec and produce working programs.</p>

<p>The result: all 26 challenges solved. Including a 1,260-line Brainfuck interpreter.</p>

<h2 id="what-mnm-lang-looks-like">What MNM Lang Looks Like</h2>

<p>Each row is one instruction. The opcode is determined by the color and length of the first token. Operands follow. <code class="language-plaintext highlighter-rouge">GGG GG</code> means <code class="language-plaintext highlighter-rouge">STORE</code> (3 greens) into variable slot 1 (2 greens, value = length - 1 = 1). That’s the entire encoding: count the candies.</p>

<h2 id="the-challenges">The Challenges</h2>

<p>I designed 26 challenges across five difficulty tiers:</p>

<p><strong>Easy (1-10):</strong> Sum 1..N, countdown, even/odd, max of two, absolute value, multiplication table, power of two, multiply by addition, integer average, sum of digits.</p>

<p><strong>Easy/Medium (11-20):</strong> M&amp;M total counter, most common color, vending machine change, staircase pattern, string length, repeat phrase, palindrome number, Collatz sequence, run-length decoding, mini gradebook (min/max/average).</p>

<p><strong>Hard (21-22):</strong> Recursive Fibonacci using <code class="language-plaintext highlighter-rouge">CALL</code>/<code class="language-plaintext highlighter-rouge">RET</code>, a tiny VM interpreter (meta-interpretation: a VM inside a VM).</p>

<p><strong>Bonus (23-25):</strong> M&amp;M histogram, digital clock with carry, Caesar cipher.</p>

<p><strong>Epic (26):</strong> A full Brainfuck interpreter.</p>

<p>The agent tackled them in order. Each challenge required reading the spec, understanding the stack semantics (pop order matters!), and producing both a <code class="language-plaintext highlighter-rouge">.mnm</code> source file and a <code class="language-plaintext highlighter-rouge">.mnm.json</code> sidecar with the right variable layout and input queues.</p>

<h2 id="what-went-wrong-at-first">What Went Wrong (At First)</h2>

<p>It wasn’t a clean sweep on the first try. Off-by-one in token encoding (35 R’s instead of 36 for ASCII 35), inverted branch polarity in the palindrome checker, variable slot collisions causing infinite loops, wrong comparison operators exiting one iteration too late. The kind of bugs you’d expect from anyone learning an unfamiliar stack machine with no type checker and no linter.</p>

<p>But the agent self-corrected every time: I pointed out the wrong output, and it re-read the spec, found the bug, and fixed it. The debugging cycle was exactly what a human would do with an unfamiliar language, just faster.</p>

<h2 id="the-brainfuck-interpreter">The Brainfuck Interpreter</h2>

<p>Challenge 26 is where things got wild. The task: implement a complete Brainfuck interpreter <em>in MNM Lang</em>. All 8 BF operations (<code class="language-plaintext highlighter-rouge">&gt; &lt; + - . , [ ]</code>), including nested bracket matching with depth tracking.</p>

<p>The fundamental problem: MNM Lang has no arrays and no indirect addressing. You can <code class="language-plaintext highlighter-rouge">LOAD var[3]</code> but you can’t <code class="language-plaintext highlighter-rouge">LOAD var[x]</code> where <code class="language-plaintext highlighter-rouge">x</code> is computed at runtime. For a BF interpreter, you need random access to both the program (to fetch the current instruction) and the tape (to read/write the current cell).</p>

<p>The solution: comparison chain dispatch. To fetch <code class="language-plaintext highlighter-rouge">program[IP]</code>, the interpreter does:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Is IP == 0? Load var[10], return.
# Is IP == 1? Load var[11], return.
# Is IP == 2? Load var[12], return.
# ... (120 cases)
</code></pre></div></div>

<p>Three subroutines (<code class="language-plaintext highlighter-rouge">fetch</code>, <code class="language-plaintext highlighter-rouge">tape_load</code>, <code class="language-plaintext highlighter-rouge">tape_store</code>), each a linear scan over all possible indices. The <code class="language-plaintext highlighter-rouge">fetch</code> subroutine alone is 120 cases. The tape subroutines are 20 each. The whole program is 1,260 lines of MNM source, using 174 labels. It was generated by a Python script (<code class="language-plaintext highlighter-rouge">generate.py</code>) that the agent also wrote.</p>

<p>And it works. Here’s what it looks like as candy:</p>

<p><img src="/assets/mnm-histogram.png" alt="M&amp;M Histogram challenge in MNM Lang" style="max-height: 500px;" /></p>

<p><em>The M&amp;M histogram challenge (53 lines), which prints horizontal bar charts of candy counts. The Brainfuck interpreter is 1,260 lines of this.</em></p>

<h2 id="cross-validation">Cross-Validation</h2>

<p>To verify the Brainfuck interpreter actually works, I co-developed with the agent a cross-validation test suite (<code class="language-plaintext highlighter-rouge">tests/test_brainfuck.py</code>). I pushed it toward this testing style: every BF program runs through both a reference Python BF interpreter and the MNM BF interpreter, and we assert identical output. Writing correct Brainfuck programs is itself non-trivial (especially with unbounded integers instead of wrapping cells), so several BF test programs needed debugging too. 39 test cases across multiple categories:</p>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>Programs</th>
      <th>Examples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Basic</td>
      <td>5</td>
      <td>Empty, single dot, increment, decrement</td>
    </tr>
    <tr>
      <td>Movement</td>
      <td>3</td>
      <td>Multi-cell read/write, bounce</td>
    </tr>
    <tr>
      <td>Loops</td>
      <td>6</td>
      <td>Simple multiply, clear, skip, nested, countdown</td>
    </tr>
    <tr>
      <td>Input</td>
      <td>3</td>
      <td>Echo, add, reverse</td>
    </tr>
    <tr>
      <td>Edge cases</td>
      <td>3</td>
      <td>No output, adjacent loops, add two inputs</td>
    </tr>
    <tr>
      <td>Real-world</td>
      <td>15</td>
      <td>Hello World, multiply, power-of-2, sum-of-N, Fibonacci</td>
    </tr>
    <tr>
      <td>Tape state</td>
      <td>4</td>
      <td>Verify final memory layout</td>
    </tr>
  </tbody>
</table>

<p>The Hello World BF program runs through the MNM interpreter in ~1.4 seconds and 1.6 million steps. It produces the correct 13 ASCII values: <code class="language-plaintext highlighter-rouge">72 101 108 108 111 32 87 111 114 108 100 33 10</code> (Hello World! plus newline).</p>

<p>The Fibonacci BF program, itself running inside the MNM BF interpreter, correctly outputs <code class="language-plaintext highlighter-rouge">[1, 1, 2, 3, 5, 8, 13]</code> for N=7. That’s a Fibonacci computation running inside a Brainfuck program running inside an MNM Lang interpreter. Three levels of interpretation.</p>

<h2 id="what-this-tells-us">What This Tells Us</h2>

<p>MNM Lang is a good test for coding agents for a few reasons:</p>

<p><strong>Unfamiliar territory.</strong> There are essentially zero MNM Lang programs in any training corpus. The agent can’t pattern-match against memorized solutions. It has to actually <em>reason</em> about the language specification.</p>

<p><strong>Hostile encoding.</strong> The token-length-as-value scheme means you can’t eyeball correctness. Is <code class="language-plaintext highlighter-rouge">GGGGGGG</code> a <code class="language-plaintext highlighter-rouge">DEC</code> instruction or a 6-green operand? Context matters, and off-by-one errors are invisible until runtime.</p>

<p><strong>No safety net.</strong> No type checker, no linter, no error messages beyond “wrong output” or “step limit exceeded”. Debugging requires mentally tracing a stack machine, which the agent did successfully.</p>

<p><strong>Scaling test.</strong> The jump from challenge 1 (10 lines) to challenge 26 (1,260 lines) is two orders of magnitude. The agent needed to invent architectural patterns (comparison chain dispatch, generated subroutines) that aren’t in any MNM Lang tutorial because there are no MNM Lang tutorials. And the approach of generating MNM programs via a Python script was a practical engineering decision the agent made on its own.</p>

<p>This is similar in spirit to what I observed with <a href="https://blog.mathieuacher.com/PrintfOrientedProgrammingCodingAgents/">Printf-Oriented Programming</a> and <a href="https://blog.mathieuacher.com/TeXCCChessEngine/">a chess engine written in LaTeX</a>. Coding agents can master unfamiliar paradigms, even absurd ones like encoding game logic inside <code class="language-plaintext highlighter-rouge">\expandafter</code> chains or <code class="language-plaintext highlighter-rouge">printf</code> format strings. But the really interesting question is <em>how far</em> they can scale. With POP, the agents hit a wall at chess-level complexity. With LaTeX, the agent produced a playable chess engine but needed extensive macro plumbing that pushed TeX to its limits. With MNM Lang, the agent pushed through to a working Brainfuck interpreter, but needed a code generator to manage the complexity. The pattern is consistent: agents handle novel paradigms well at small scale, and find creative workarounds (compilers, generators, meta-programming) when direct coding becomes intractable.</p>

<h2 id="try-it-yourself">Try It Yourself</h2>

<p>The fork with all 26 challenges is at <a href="https://github.com/acherm/mnmlang">github.com/acherm/mnmlang</a>. Each challenge has a <code class="language-plaintext highlighter-rouge">.mnm</code> source and sidecar JSON. Run any of them with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run mnm run challenges/01_sum_1_to_n/sum.mnm
uv run mnm run challenges/26_brainfuck/bf.mnm
</code></pre></div></div>

<p>The cross-validation test suite:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run <span class="nt">--extra</span> dev pytest tests/test_brainfuck.py <span class="nt">-v</span>
</code></pre></div></div>

<div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">acher2026mnmlang</span><span class="p">,</span>
  <span class="na">author</span> <span class="p">=</span> <span class="s">{Mathieu Acher}</span><span class="p">,</span>
  <span class="na">title</span> <span class="p">=</span> <span class="s">{Can Coding Agents Program in M&amp;Ms Language?}</span><span class="p">,</span>
  <span class="na">year</span> <span class="p">=</span> <span class="s">{2026}</span><span class="p">,</span>
  <span class="na">month</span> <span class="p">=</span> <span class="s">{mar}</span><span class="p">,</span>
  <span class="na">howpublished</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CodingAgentsMnMLang/}}</span><span class="p">,</span>
  <span class="na">note</span> <span class="p">=</span> <span class="s">{\url{https://blog.mathieuacher.com/CodingAgentsMnMLang/}}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="LLM" /><category term="coding agents" /><category term="esoteric languages" /><category term="MNM Lang" /><category term="Brainfuck" /><category term="generative AI" /><category term="llm4code" /><category term="Claude Code" /><summary type="html"><![CDATA[What happens when you challenge a coding agent with an esoteric programming language it has never seen (freshly invented, with very few examples), and where the source code is literally colored M&amp;Ms on a table? No documentation beyond the opcode table. No Stack Overflow threads. No training examples to memorize. Just six candy colors, a stack machine, and a dare. I tried it with MNM Lang, a toy language amazingly created by Mufeed VH where programs are grids of candy-colored tokens (Blue, Green, Red, Yellow, Orange, browN). Token length encodes operand values. Strings and inputs live in a sidecar JSON file. The whole thing compiles to a PNG of M&amp;M sprites. So funny by design, for humans. But could an LLM learn to write it?]]></summary></entry></feed>