Tour du Pain Gratis rijden

Hoe ik Tour du Pain bouw met Claude Code

Als videomaker die geen code schrijft

Wouter op een grindweg met zijn fiets, naast een weg in Tour du Pain: drie rijders op de Col du Pain met vermogen, hartslag, de rijderslijst en de chat. Titel: Hoe ik een fietsgame bouw met Claude

Ik maak video's voor mijn werk. Een game had ik nooit geschreven, en TypeScript schrijven lukt me nog steeds nauwelijks. Op 22 oktober zette ik een klein testje op dat Rollenbank heette. Eén pagina. Die las mijn Wahoo KICKR Core uit via Bluetooth en schoof een stipje over een weg. Drie weken later heet het Tour du Pain en staan er ruim 140 releases op de teller, met twee klimmen, voicechat, clubs en rijders op de weg die ik nooit ontmoet heb.

Helemaal vanaf nul begon ik niet. Jaren geleden studeerde ik CMD (Communicatie, Multimedia en Design), een mix van ontwerp, interactie en media. Afgemaakt heb ik het nooit. Wat bleef hangen, is gevoel voor hoe schermen en systemen in elkaar grijpen. En als filmmaker weet ik hoe een goed shot eruitziet: waar het licht vandaan komt, waar je oog als eerste naartoe gaat, wanneer een beeld te druk is. Dat bleek belangrijker dan ik dacht, want de meeste van mijn notities aan Claude gaan precies daarover.

Drie rijders trappen bij zonsondergang onder de Arc de Triomphe door op het meerrondje, met tarwe en lavendel langs de weg
Onder de Arc de Triomphe door op het meerrondje. Gefilmd in de game op 2x, zonder geluid.

Alles is gebouwd met Claude Code. Gaandeweg raakte ik een beetje geobsedeerd door de vraag hoe je dat goed aanpakt, dus dit is de lange versie: hoe ik het werk over parallelle sessies verdeel, welk model wat doet, waarom er niets live gaat zonder bewijs en hoe apparaatlogs van echte rijders fixes worden. Bouw je zelf zoiets, neem dan mee wat je kunt gebruiken.

Inhoud 12 delen
  1. Werken zoals op een filmset
  2. Eerst de briefing, dan pas code
  3. Het geheugen van het project
  4. Zes sessies, zes werkgebieden
  5. Welk model waarvoor
  6. Bewijs, anders is het niet af
  7. Wat de logs van echte rijders laten zien
  8. Een bug die je alleen voelt
  9. Zes rondes voor één klim
  10. Regels sinds er vreemden meerijden
  11. Nog steeds veel werk
  12. Zelf beginnen

Werken zoals op een filmset

Mijn werk lijkt het meest op een film maken. Er is een script, een shotlijst, een draaidag en een montage, en ik beslis wat het eindresultaat wordt. Claude schrijft de code, draait hem, test hem en vertelt wat er gecontroleerd is. Daarna stap ik op de fiets en zeg ik waar het wringt.

Code lees ik niet regel voor regel, en diffs bekijk ik ook niet. Ik beoordeel wat ik zie en voel (is die klim echt steil, valt de voice weg zodra er iemand bijkomt) en klopt er iets niet, dan krijgt Claude een screenshot met één zin uitleg. Zo geef ik ook notities aan een editor. De technische discipline zit in het systeem rondom de sessies, en daar gaat de rest van deze post over.

Drie rijders klimmen samen de Col du Pain op met het ritscherm: vermogen, ERG, hartslag, de klimkaart, de rijderslijst met wie praat, en de chat
Met z'n drieën op de Col du Pain: iedereen trapt, eerst praat Sanne en dan Joost, en rechts loopt de chat. Gefilmd in de game, zonder geluid.

Op het schema hieronder zie je die hele machine. Zes sessies beheren elk een stuk van de code op hun eigen git-branch, en ze openen allemaal als eerste hetzelfde projectbestand. Tussen al die sessies en de live site staan precies één deploy-slot en één set checks.

Zes gebieden, elk met een eigen sessie en branch. Er gaat er maar één tegelijk door het slot en de checks.

Eerst de briefing, dan pas code

Er wordt pas iets gebouwd als er een briefing ligt. Dat is een Markdown-bestand dat de functie beschrijft alsof hij al bestaat. Met de eerste, PROMPT-TOUR-DU-PAIN.md, werd mijn trainertestje een spel. Die briefing bepaalt de toon ("grappig, een beetje absurd, met een Frans tintje, en altijd makkelijk te snappen"), geeft elk spelonderdeel één regel en noemt wat er buiten valt.

Dat is een regel uit die briefing, letterlijk overgenomen. Hij past in één zin, en dan is testen makkelijk.

Zo'n briefing ontstaat in stappen. Ik praat het idee door met Claude en laat het doorvragen tot alles scherp is. Dan loopt er een aparte ronde overheen met een checklist die ik daarvoor bijhoud: valt elke regel te testen, wat gebeurt er offline, wat als twee rijders dit op hetzelfde moment doen, en wat is de kleinste versie die al het uitbrengen waard is. Pas dan mag een sessie bouwen, het liefst in één run.

Nadenken en bouwen houd ik in gescheiden sessies. Een bouwsessie met een half idee maakt namelijk keurig en vol enthousiasme het verkeerde ding.

Waar onderdelen met elkaar praten, liggen er naast de briefings ook contractbestanden, zoals CONTRACTS-ONLINE.md en CONTRACTS-CONNECT.md. Elk bericht tussen spel en server staat daarin vast. Volgens de afspraak in het project veranderen shared/protocol.ts en die contracten altijd tegelijk. Zo werken de multiplayersessie en de serversessie elk aan hun eigen kant van de lijn, zonder bij de ander iets te slopen.

Het geheugen van het project

Het eerste wat elke sessie leest, is CLAUDE.md in de hoofdmap. Bij mij beslaat dat zo'n twee pagina's: de techniek, een tabel met wie welke mappen beheert, de huisregels en de releasestappen. Een greep eruit:

Met dat bestand hoef ik mezelf nooit te herhalen. De sessie die morgen opstart, werkt met dezelfde regels als de sessie die ze opschreef, en alles wat ik voor de tweede keer moet corrigeren, gaat erin.

Zes sessies, zes werkgebieden

Zolang het spel klein was, paste alles in één lang gesprek. Op een gegeven moment ging dat mis. Claude was kwijt wat het een uur eerder had besloten en elke wijziging raakte overal aan. Toen heb ik de code verdeeld zoals een filmcrew het werk verdeelt:

GebiedBeheert
Rijden en trainersNatuurkunde, Bluetooth, trainers, workouts
Peloton en botsHet verkeer en de botrijders, zoals Kristian
Wereld en beeldHet parcours, het landschap, publiek, licht, camera
Multiplayer, chat en voiceNetcode, voice, de lobby en de serverkamers
Accounts en rittenAccounts, ritten opslaan, Strava, Garmin
Menu's, ontwerp en taalAlle schermen, beide talen

Ze delen allemaal dezelfde map op mijn Mac, dus elke sessie krijgt een eigen git-worktree met een eigen branch. Heeft de wereldsessie iets nodig in de menu's, dan blijft die wijziging piepklein of gaat hij door naar de menusessie. Sessies sturen elkaar ook berichten. Ziet er een in het spellog een probleem met een Zwift Hub, dan gaat dat rechtstreeks naar de sessie die Bluetooth beheert, zonder dat ik iets hoef te kopiëren.

Mijn eigen feedback gaat ook naar de eigenaar. "Kristian rijdt de hele tijd naast me" is voor het peloton, en met "de keuze voor de klim is te klein op een tablet" gaan de menu's aan de slag.

Welk model waarvoor

Het grootste model is lang niet altijd nodig. Na een paar weken naar mijn gebruik kijken, werk ik zo. Alles wat code schrijft of aanpast, draait op Opus. Plannen en controleren gebeurt in een aparte denkronde: briefings en reviews krijgen een eigen sessie, net als elk verzoek om een tweede mening, zodat de bouwer nooit zijn eigen rechter is. Sonnet en Haiku doen het routinewerk, zoals browsen, een pagina nakijken, een formulier invullen of logs doorzoeken op één patroon. Dat is goedkoper en snel genoeg.

Voor brede zoektochten zijn er subagents. Moeten er voor één vraag twintig bestanden gelezen worden, dan doet een subagent dat en brengt hij alleen de conclusie mee terug. Dat scheelt meer dan je zou denken. De context van een sessie is zijn werkgeheugen, en een sessie die je volstopt met hele bestanden wordt slechter in zijn eigenlijke werk.

Bewijs, anders is het niet af

Hier ben ik streng. Meldt een model dat iets werkt, dan wil ik weten wat er gecontroleerd is. Geen release gaat naar buiten voordat de volledige set checks groen is:

  • Typechecks voor spel en server, in strikte modus.
  • Unittests op vier workers met een time-out van 60 seconden. Met de standaardinstellingen lopen ze vast zodra zes sessies één computer delen.
  • sim-check draait de complete simulatie headless, met natuurkunde, workouts, hartslag, de bots en het parcours.
  • game-check doet hetzelfde voor de spellaag: de stokbroodeconomie, de koffiestop, je profiel en je plek op het parcours.
  • verify-fit neemt headless ritten op en controleert de .fit-bestanden byte voor byte, zodat klopt wat er in Strava en Garmin terechtkomt.

release 1.39.4 · één sessie heeft het deploy-slot

$ npm run deploy
deploy lock✓geclaimd door deze sessietsc✓game en server, strict, geen foutenvitest✓238 bestanden, 2895 tests geslaagdsim-check✓geslaagd (250 W → 36,6 km/h)game-check✓geslaagd (5 stokbroden op 26 km)verify-fit✓geslaagd (Decoder.isFIT) 633 checks over sim, game en .fitvite build✓gebouwd in 706 msd1 migrations✓niets te doenwrangler deploy✓tourdupain-appindexnow✓1 URL gemeld, antwoord 200
$ curl tourdupain.app/api/version
{"version":"1.39.4"}
Een echte release (1.39.4), ingekort tot één regel per check, met het versie-endpoint als bewijs.

Na de deploy opent de sessie de live site, leest de browserconsole en controleert of /api/version het nieuwe nummer geeft. Pas daarna mag "live als 1.39.4" in het verslag. Kan de sessie het niet aantonen, dan geldt het voor mij als niet live.

Prestaties meet ik op dezelfde manier. Een benchmarkscript kan elke commit bouwen, draait hem in headless Chrome op de echte videokaart en laat de camera door de wereld vliegen. Een grafische wijziging telt pas als verbetering als de cijfers dat laten zien. Neem een oudere laptop, nagebootst door de processor vier keer te vertragen: daarop haalt grafische stand Medium 39 tot 54 fps met 50 rijders in beeld. Dat getal komt uit zo'n meting.

Voor multiplayer is er een belastingtest die 50 nagebootste rijders tegelijk door de lobby stuurt. Elk daarvan doet wat een echt spel doet. Ze komen in kleine groepjes binnen en geven vele keren per seconde hun positie door.

  • Ronde alleen, mediaan
  • 50 rijders, beste run
  • 50 rijders, cpu 4× trager
  • 60 fps
01002003004006038827655–90Laag33021439–54Gemiddeld21016337–57Hoog60 fpsLaagRonde alleen38850 rijders276Cpu 4× trager55–90GemiddeldRonde alleen33050 rijders214Cpu 4× trager39–54HoogRonde alleen21050 rijders163Cpu 4× trager37–57
Gemeten op 30 oktober 2026, voor die ronde snelheidswerk, in headless Chrome op de echte gpu. De cpu-4×-bereiken staan voor een oudere laptop.

Wat de logs van echte rijders laten zien

Bij elke rit kan een klein apparaatlog mee. Daarin staat welke trainer verbonden was, welke vakjes hij vulde, hoe oud zijn data was en welke fouten Bluetooth gaf. Een sessie leest die logs en maakt er een audit van, met per probleem de oorzaak plus het bestand en regelnummer.

Vanochtend zat er een mooie tussen. Een rijder had een Zwift Hub als trainer én als hartslagbron gekoppeld, en na het herladen van de pagina kwam geen van beide terug. Het log toonde twee fouten in dezelfde milliseconde. De code voor opnieuw verbinden bleek hetzelfde Bluetooth-apparaat twee keer te vragen naar zijn signaal te luisteren, voor elk vakje één keer, en Chrome laat per apparaat maar één openstaande luisteropdracht toe. Dus faalde de tweede. Bij het opruimen daarvan brak ook de eerste af, waarna beide vakjes het opgaven.

Apparaatlog · één rijder · 10 nov · na herladen

16:42:02heartRateZwift Hub from last time: Failed to execute 'watchAdvertisements': pending watch operation.

16:42:02trainerZwift Hub from last time: The Bluetooth operation was cancelled.

Dezelfde seconde: de tweede watch faalt, en het opruimen daarvan annuleert de eerste.

Oorzaak

Opnieuw verbinden vroeg hetzelfde Bluetooth-apparaat twee keer om te luisteren, één keer per slot. Chrome staat per apparaat één openstaande watch toe.

Oplossing, live in 1.40.3

Eén watch per apparaat, daarna elk slot verbinden dat hem wil. Een nep-Hub die zich gedraagt als Chrome draait nu bij elke release.

De twee fouten uit het log, de oorzaak en de oplossing.

Tegenwoordig luistert de code één keer per apparaat en koppelt hij daarna elk vakje dat het signaal wil hebben. Uit de audit kwam ook een test: een nep-Zwift Hub die, net als Chrome, een tweede luisteropdracht weigert zolang er al één openstaat. Die draait nu bij elke release mee. Al rijdend had ik dit zelf nooit gevonden, want ik heb niet eens een Zwift Hub.

Een bug die je alleen voelt

Sommige bugs merk je alleen door te rijden. Reed je thuis via wifi samen, dan sprong de andere rijder af en toe naar voren en zakte hij weer terug. Zijn posities kwamen in klontjes binnen en het spel tekende ze precies zo, op het moment dat ze aankwamen.

Sinds 1.37.6 kijkt het spel niet meer naar aankomsttijden. Het leidt de timing van elke rijder af uit zijn eigen snelheid. Na de reparatie heeft de sessie de situatie ook nagebootst. Op een volle router bleven er van 110 sprongen in drie minuten 5 over, en de andere rijder wordt nu binnen 2,4 meter van zijn echte plek getekend, waar dat eerst 12,8 meter was.

110 → 5sprongen in drie minuten op een volle router

12,8 → 2,4 mzo ver tekent de game de andere rijder van de echte plek

Voor 1.37.6110 sprongen in 3 min · tot 12,8 m Na 1.37.65 sprongen in 3 min · binnen 2,4 m 0 m5 m10 m15 m echte plekzoals getekend
Voor en na 1.37.6, op schaal: de ring is waar de andere rijder echt is, de stip is waar de game die tekent.

Zes rondes voor één klim

De eerste Col du Baguette was in korte tijd gebouwd, en zo zag hij er ook uit. Naast mijn eigen schets gelegd kreeg hij van mij een 4 uit 10. Daarna is hij in zes fasen opnieuw opgebouwd: eerst het licht, toen de bergen, de sneeuw, het gehucht, het publiek en als laatste de top. Na elke fase schoot de sessie screenshots vanaf dezelfde camerapunten. Die legde ik naast de schets, waarna ik besliste of de klim door mocht. Na de laatste fase gaf ik hem een 8.

De lantaarns en fakkels zijn een detail waar ik blij van word. Ze verlichten de weg, de sneeuw, de huizen en de gezichten van de fans, terwijl dat licht gewoon in de kleuren van de 3D-modellen is ingebakken. Per frame kost het niets, dus ook een oude laptop trekt het.

De Mur de Feu in 1.38.0: een donkerblauwe nacht, rijder en fans bijna onverlicht, een vlag over de weg
1.38.0, eerste versie (4/10)
De Mur de Feu in 1.39.0 vanuit dezelfde camera: warm fakkellicht op de weg, de rijder en de fans
1.39.0, na zes fasen (8/10)
Drie rijders klimmen 's nachts de Mur de Feu op de Col du Baguette op tussen rijen fans met fakkels, met besneeuwde toppen erachter
De Mur de Feu op de Col du Baguette, bij nacht.

Regels sinds er vreemden meerijden

Het spannendste moment tot nu toe was de eerste avond dat ik rijders op de weg zag die geen vrienden van me waren. Vanaf toen kon een release iemand midden in een rit eruit gooien. De regels werden strenger, en de meeste zitten inmiddels in scripts.

Er loopt maar één deploy tegelijk. Voordat een sessie iets live zet, draait hij node scripts/deploy-lock.mjs take, en een tweede sessie die ook wil deployen moet wachten. Laat het slot 20 minuten geen teken van leven zien, dan gaat het vrij, zodat een gecrashte sessie de rest niet ophoudt. Wil ik alles stilleggen, dan zet ik een bestand DEPLOY_FREEZE in de repo en weigert het deployscript tot ik het weer weghaal.

Het protocol moet achterwaarts compatibel blijven, want rijders herladen niet midden in een rit. Een nieuwe server accepteert dus de spelversie die nu live staat. Bij een deploy herstarten de kamers en valt elke rijder een paar seconden weg, waarna hij opnieuw verbindt met een hervatbericht en zijn rit gewoon doorloopt. Databasewijzigingen voegen alleen toe, in de vorm van nieuwe tabellen en kolommen. Hernoemen of weggooien gebeurt nooit in dezelfde release. Verder krijgt elke release een versienummer met twee notities: een technische regel in de changelog en een gewone voor rijders op de updatespagina.

Saaie regels, maar dankzij die regels kunnen zes sessies tegelijk werken en slaap ik toch rustig.

De updatespagina van Tour du Pain: de vier nieuwste releases van 14 november 2026, elk met in gewone woorden wat er veranderde
De updatespagina: elke release met datum, in gewone woorden.

Nog steeds veel werk

Tour du Pain heeft me deze weken heel veel uren gekost. Avonden en weekenden, en ritjes waarin ik halverwege stopte om op te schrijven wat niet goed voelde. Claude schrijft de code, maar iemand moet nog steeds bepalen wat het spel is, elke versie uitproberen, weggooien wat niet werkt en nee zeggen tegen een hoop goede ideeën. Die iemand ben ik, en dat kost tijd.

Wat veranderd is, is wie het kan doen. Een game als deze kost normaal een groot team en bijna een jaar. Ik bouwde hem in mijn eentje in een paar weken. Dat lukt alleen als je precies weet wat je wilt maken, en net zo goed wat je níet gaat maken. De visie moet nog steeds van jou komen. Met Claude kun je hem alleen dragen.

Zelf beginnen

Programmeren hoef je niet te kunnen. Je moet wel precies weten wat je wilt en streng zijn over hoe het gecontroleerd wordt. Dit hielp mij het meest.

Bouw iets wat je zelf gebruikt. Ik rijd dit spel met vrienden, dus veel problemen kom ik als eerste tegen. Schrijf de briefing voordat er code komt, en laat een andere sessie hem nakijken dan de sessie die gaat bouwen. Houd één CLAUDE.md bij en zet er elke correctie in die je voor de tweede keer maakt.

Verdeel het werk op eigenaarschap, met één gebied per sessie en elke sessie in een eigen worktree. De context blijft dan klein en de wijzigingen netjes. Kies het model per klus: het sterkste om te bouwen en goedkopere voor routine. Voor brede zoektochten zet je subagents in.

Vraag om bewijs. Een sessie die "het is klaar" meldt, moet dat kunnen onderbouwen, bijvoorbeeld met "live als 1.39.4, de versiepagina laat het zien en de console is leeg". Leg je regels vast in scripts, want die onthouden wat jij om elf uur 's avonds vergeet. Bij mij zijn dat een slot en een freeze-bestand, plus de vaste set checks. En log wat echte gebruikers tegenkomen, omdat de belangrijkste bugs gebeuren op hardware die je zelf niet hebt.

Er gaat nog steeds van alles stuk, toverij is het niet, en ik zit heel wat avonden op de trainer om een klim voor de vijfde keer te testen. Dat hoort bij het leuke. Ik maak al jaren films, en dit is het eerste wat ik regisseer waar mensen daarna live samen in rijden.

Wil je zien wat eruit gekomen is, probeer het spel dan zonder trainer. Loop je vast terwijl je zelf iets met Claude bouwt, mail me dan op info@tourdupain.com. Ik praat er graag over.

Lees ook