Gepubliceerd: 10 oktober 2025
Het klassieke bordspel ' Wie is het? ' is een masterclass in deductief redeneren. Elke speler begint met een bord met plaatjes en beperkt, door middel van een reeks ja-/nee-vragen, de mogelijkheden totdat je het geheime personage van je tegenstander met zekerheid kunt identificeren.
Nadat ik een demo van ingebouwde AI had gezien op Google I/O Connect, vroeg ik me af: wat als ik een 'Wie is het?'-spel zou kunnen spelen tegen een AI die in de browser zit? Met client-side AI zouden de foto's lokaal worden geïnterpreteerd, zodat een eigen 'Wie is het?'-spel van vrienden en familie privé en veilig op mijn apparaat zou blijven.
Mijn achtergrond ligt voornamelijk in UI- en UX-ontwikkeling, en ik ben gewend om pixelperfecte ervaringen te bouwen. Ik hoopte dat ik precies dat met mijn interpretatie zou kunnen doen.
Mijn applicatie, AI Guess Who?, is gebouwd met React en gebruikt de Prompt API en een in de browser ingebouwd model om een verrassend capabele tegenstander te creëren. Tijdens dit proces ontdekte ik dat het niet zo eenvoudig is om "pixelperfecte" resultaten te behalen. Maar deze applicatie laat zien hoe AI kan worden gebruikt om doordachte spellogica te ontwikkelen, en hoe belangrijk prompt engineering is om deze logica te verfijnen en de resultaten te behalen die je verwacht.
Lees verder om meer te weten te komen over de ingebouwde AI-integratie, de uitdagingen waar ik voor stond en de oplossingen die ik heb gevonden. Je kunt het spel spelen en de broncode vinden op GitHub .
Game foundation: een React-app
Voordat we naar de AI-implementatie kijken, bekijken we de structuur van de applicatie. Ik heb een standaard React-applicatie gebouwd met TypeScript, met een centraal App.tsx
bestand dat als dirigent van het spel fungeert. Dit bestand bevat:
- Spelstatus : een opsomming die de huidige fase van het spel bijhoudt (zoals
PLAYER_TURN_ASKING
,AI_TURN
,GAME_OVER
). Dit is het belangrijkste onderdeel van de status, omdat het bepaalt wat de interface weergeeft en welke acties beschikbaar zijn voor de speler. - Karakterlijsten : Er zijn meerdere lijsten met de actieve karakters, het geheime karakter van elke speler en welke karakters van het bord zijn verwijderd.
- Gamechat : een lopend logboek met vragen, antwoorden en systeemberichten.
De interface is onderverdeeld in logische componenten:


Naarmate de functies van de game toenamen, nam ook de complexiteit toe. Aanvankelijk werd de hele logica van de game beheerd binnen één grote, aangepaste React-hook , useGameLogic
, maar deze werd al snel te groot om te navigeren en te debuggen. Om de onderhoudbaarheid te verbeteren, heb ik deze hook geherstructureerd in meerdere hooks, elk met één verantwoordelijkheid. Bijvoorbeeld:
-
useGameState
beheert de kernstatus -
usePlayerActions
is voor de beurt van de speler -
useAIActions
is voor de logica van de AI
De belangrijkste useGameLogic
hook fungeert nu als een overzichtelijke composer, waarbij deze kleinere hooks samen worden geplaatst. Deze architectuurwijziging heeft de functionaliteit van de game niet veranderd, maar de codebase is er wel een stuk overzichtelijker door geworden.
Spellogica met de Prompt API
De kern van dit project is het gebruik van de Prompt API.
Ik heb de AI-gamelogica toegevoegd aan builtInAIService.ts
. Dit zijn de belangrijkste verantwoordelijkheden:
- Sta beperkende, binaire antwoorden toe.
- Leer de strategie van het modelspel.
- Leer de modelanalyse.
- Geef het model geheugenverlies.
Beperkende, binaire antwoorden toestaan
Hoe werkt de speler samen met de AI? Wanneer een speler vraagt: "Heeft je personage een hoed?", moet de AI naar de afbeelding van zijn geheime personage "kijken" en een duidelijk antwoord geven.
Mijn eerste pogingen liepen op een mislukking uit. Het antwoord was een alledaags antwoord: "Nee, het personage waar ik aan denk, Isabella, lijkt geen hoed te dragen", in plaats van een binair ja of nee. Aanvankelijk loste ik dit op met een zeer strikte prompt, waarbij ik het model in feite dicteerde om alleen met "Ja" of "Nee" te antwoorden.
Terwijl dit werkte, leerde ik een nog betere manier kennen, namelijk gestructureerde output . Door het JSON-schema aan het model te verstrekken, kon ik een waar/onwaar-antwoord garanderen.
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
Hierdoor kon ik de prompt vereenvoudigen en mijn code de respons betrouwbaar laten verwerken:
JSON.parse(result) ? "Yes" : "No"
Leer de modelspelstrategie
Het model een vraag laten beantwoorden is veel eenvoudiger dan het model zelf vragen te laten stellen. Een goede 'Wie is het?'-speler stelt geen willekeurige vragen. Hij stelt vragen die in één keer de meeste personages elimineren. Een ideale vraag halveert het aantal mogelijke resterende personages door binaire vragen te stellen.
Hoe leer je een model die strategie? Nogmaals, prompt engineering. De prompt voor generateAIQuestion()
is eigenlijk een beknopte les in 'Wie is het?'-speltheorie.
Aanvankelijk vroeg ik het model om "een goede vraag te stellen". De resultaten waren onvoorspelbaar. Om de resultaten te verbeteren, voegde ik negatieve beperkingen toe. De prompt bevat nu instructies die vergelijkbaar zijn met:
- "KRITISCH: Vraag ALLEEN naar bestaande functies"
- KRITISCH: Wees origineel. Herhaal GEEN vraag.
Deze beperkingen beperken de focus van het model en voorkomen dat het irrelevante vragen stelt, wat het een veel leukere tegenstander maakt. Je kunt het volledige promptbestand bekijken op GitHub .
Leer de modelanalyse
Dit was verreweg de moeilijkste en belangrijkste uitdaging. Als het model een vraag stelt, zoals: "Heeft je personage een hoed?", en de speler antwoordt nee, hoe weet het model dan welke personages op zijn bord zijn uitgeschakeld?
Het model zou iedereen met een hoed moeten elimineren. Mijn eerste pogingen zaten vol met logische fouten, en soms elimineerde het model de verkeerde tekens of helemaal geen tekens. En wat is een "hoed"? Telt een "muts" ook als een "hoed"? Dit is, laten we eerlijk zijn, ook iets wat kan gebeuren in een menselijk debat. En natuurlijk gebeuren er wel eens algemene fouten. Haar kan er vanuit een AI-perspectief uitzien als een hoed.
Ik heb de architectuur opnieuw ontworpen om perceptie te scheiden van code-deductie:
AI is verantwoordelijk voor visuele analyse . Modellen blinken uit in visuele analyse. Ik heb het model geïnstrueerd om zijn vraag en een gedetailleerde analyse te retourneren in een strikt JSON-schema. Het model analyseert elk personage op zijn bord en beantwoordt de vraag: "Heeft dit personage deze eigenschap?" Het model retourneert een gestructureerd JSON-object:
{ "character_id": "...", "has_feature": true }
Ook hier zijn gestructureerde gegevens de sleutel tot een succesvol resultaat.
De gamecode gebruikt de analyse om de uiteindelijke beslissing te nemen . De applicatiecode controleert het antwoord van de speler ("Ja" of "Nee") en herhaalt de analyse van de AI. Als de speler "Nee" antwoordt, weet de code dat elk teken waarvoor
has_feature
true
is, moet worden verwijderd.
Ik ontdekte dat deze taakverdeling essentieel is voor het bouwen van betrouwbare AI-applicaties. Gebruik de AI vanwege de analytische mogelijkheden en laat binaire beslissingen over aan je applicatiecode.
Om de perceptie van het model te controleren, maakte ik een visualisatie van deze analyse. Dit maakte het gemakkelijker om te bevestigen of de perceptie van het model correct was.
Snelle engineering
Maar zelfs met deze scheiding merkte ik dat de perceptie van het model nog steeds gebrekkig kon zijn. Het kon verkeerd inschatten of een personage een bril droeg, wat leidde tot een frustrerende, onterechte eliminatie. Om dit te verhelpen, experimenteerde ik met een tweestaps-proces: de AI stelde zijn vraag. Na ontvangst van het antwoord van de speler voerde hij een tweede, frisse analyse uit met het antwoord als context. De theorie was dat een tweede blik fouten uit de eerste blik zou kunnen opsporen.
Dit is hoe de stroom zou hebben gewerkt:
- AI-beurt (API-aanroep 1) : AI vraagt: "Heeft je personage een baard?"
- De beurt aan de speler : De speler kijkt naar zijn geheime personage, die gladgeschoren is, en antwoordt: "Nee."
- AI-beurt (API-aanroep 2) : De AI vraagt zichzelf om nogmaals naar alle overgebleven personages te kijken en te bepalen welke personages op basis van het antwoord van de speler moeten worden geëlimineerd.
In stap twee kan het model een personage met een lichte stoppelbaard nog steeds verkeerd interpreteren als "geen baard hebbend" en deze niet elimineren, ondanks dat de gebruiker dat wel verwachtte. De kernfout in de perceptie werd niet hersteld en de extra stap vertraagde de resultaten alleen maar. Wanneer we tegen een menselijke tegenstander spelen, kunnen we hierover een overeenkomst of verduidelijking afspreken; in de huidige opstelling met onze AI-tegenstander is dit niet het geval.
Dit proces zorgde voor extra latentie bij een tweede API-aanroep, zonder dat de nauwkeurigheid significant verbeterde. Als het model de eerste keer fout was, was het vaak de tweede keer ook fout. Ik heb de prompt slechts één keer teruggezet naar 'review'.
Verbeteren in plaats van meer analyses toevoegen
Ik baseerde mij op een UX-principe: de oplossing was niet meer analyse, maar betere analyse.
Ik heb flink geïnvesteerd in het verfijnen van de prompt en expliciete instructies toegevoegd om het model te laten controleren en zich te concentreren op specifieke kenmerken. Dit bleek een effectievere strategie om de nauwkeurigheid te verbeteren. Zo werkt de huidige, betrouwbaardere flow:
AI-beurt (API-aanroep) : het model wordt gevraagd om zowel de vraag als de interne analyse tegelijkertijd te genereren en één enkel JSON-object te retourneren.
- Vraag : "Draagt jouw personage een bril?"
- Analyse (gegevens) :
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
Beurt van de speler : Het geheime karakter van de speler is Alex (geen bril), dus hij antwoordt: "Nee."
Einde van de ronde : de JavaScript-code van de applicatie neemt het over. Deze hoeft de AI verder niets te vragen. De analysegegevens van stap 1 worden doorlopen.
- De speler zei "Nee."
- De code zoekt naar elk teken waarbij
has_feature
true is. - Het draait Brad en Gina om. De logica is deterministisch en direct.
Dit experiment was cruciaal, maar vergde veel vallen en opstaan. Ik had geen idee of het beter zou worden. Soms werd het zelfs erger. Bepalen hoe je de meest consistente resultaten krijgt, is geen exacte wetenschap (nog niet, als dat ooit gebeurt...).
Maar na een paar rondes met mijn nieuwe AI-tegenstander, deed zich een fantastisch nieuw probleem voor: een patstelling.
Ontsnap aan impasse
Als er nog maar twee of drie zeer vergelijkbare personages overbleven, raakte het model in een lus. Het stelde dan een vraag over een kenmerk dat ze allemaal deelden, zoals: "Draagt jouw personage een hoed?"
Mijn code zou dit correct identificeren als een verspilde beurt, en de AI zou een andere, even brede functie proberen die alle personages ook deelden, zoals: "Draagt je personage een bril?"
Ik heb de prompt verbeterd met een nieuwe regel: als een poging om een vraag te genereren mislukt en er nog maar drie of minder tekens over zijn, verandert de strategie.
De nieuwe instructie is expliciet: "In plaats van een algemeen kenmerk, moet je vragen naar een specifieker, uniek of gecombineerd visueel kenmerk om een verschil te vinden." Zo wordt bijvoorbeeld in plaats van de vraag of het personage een hoed draagt, gevraagd of hij of zij een honkbalpet draagt.
Hierdoor moet het model veel beter naar de beelden kijken om dat ene kleine detail te vinden dat uiteindelijk tot een doorbraak kan leiden. Hierdoor werkt de strategie in de late game in de meeste gevallen iets beter.
Geef het model geheugenverlies
De grootste kracht van een taalmodel is zijn geheugen. Maar in dit spel werd die grootste kracht een zwakte. Toen ik een tweede spel begon, stelde het verwarrende of irrelevante vragen. Natuurlijk bewaarde mijn slimme AI-tegenstander de volledige chatgeschiedenis van het vorige spel. Hij probeerde twee (of zelfs meer) spellen tegelijk te begrijpen.
In plaats van dezelfde AI-sessie opnieuw te gebruiken, vernietig ik deze nu expliciet aan het einde van elk spel, waardoor de AI feitelijk geheugenverlies krijgt.
Wanneer je op Opnieuw afspelen klikt, reset de functie startNewGameSession()
het bord en creëert een gloednieuwe AI-sessie. Dit was een interessante les in het beheren van de sessiestatus, niet alleen in de app, maar ook binnen het AI-model zelf.
Extraatjes: aangepaste spellen en spraakinvoer
Om de ervaring nog aantrekkelijker te maken, heb ik twee extra functies toegevoegd:
Aangepaste tekens : Met
getUserMedia()
kunnen spelers hun camera gebruiken om hun eigen set van 5 tekens te maken. Ik heb IndexedDB gebruikt om de tekens op te slaan, een browserdatabase die perfect is voor het opslaan van binaire gegevens zoals afbeeldingsblobs. Wanneer je een aangepaste set maakt, wordt deze opgeslagen in je browser en verschijnt er een afspeeloptie in het hoofdmenu.Spraakinvoer : Het client-side model is multimodaal . Het kan tekst, afbeeldingen en ook audio verwerken. Door de MediaRecorder API te gebruiken om microfooninvoer vast te leggen, kon ik de resulterende audioblob naar het model sturen met de prompt: "Transcribeer de volgende audio...". Dit voegt een leuke manier van spelen toe (en een leuke manier om te zien hoe het mijn Vlaamse accent interpreteert). Ik heb dit vooral gemaakt om de veelzijdigheid van deze nieuwe webfunctie te laten zien, maar eerlijk gezegd was ik het zat om steeds maar weer vragen te typen.
Laatste gedachten
Het bouwen van "AI Raad eens Wie?" was absoluut een uitdaging. Maar met een beetje hulp van het lezen van documentatie en wat AI om AI te debuggen (ja... dat heb ik gedaan), bleek het een leuk experiment. Het benadrukte de immense potentie van het draaien van een model in de browser voor het creëren van een privé, snelle ervaring zonder internet. Dit is nog steeds een experiment, en soms speelt de tegenstander gewoon niet perfect. Het is niet pixelperfect of logisch perfect. Met generatieve AI zijn de resultaten afhankelijk van het model.
In plaats van te streven naar perfectie, streef ik ernaar het resultaat te verbeteren.
Dit project onderstreepte ook de constante uitdagingen van prompt engineering. Die prompting werd echt een belangrijk onderdeel ervan, en niet altijd het leukste. Maar de belangrijkste les die ik leerde, was het ontwerpen van de applicatie om perceptie en deductie te scheiden, en zo de mogelijkheden van AI en code te scheiden. Zelfs met die scheiding ontdekte ik dat de AI nog steeds (voor een mens) voor de hand liggende fouten kon maken, zoals tatoeages verwarren met make-up of de draad kwijtraken van wiens geheime personage er werd gesproken.
De oplossing was om de prompts steeds duidelijker te maken, door instructies toe te voegen die voor een mens vanzelfsprekend zijn, maar essentieel voor het model.
Soms voelde de game oneerlijk. Soms had ik het gevoel dat de AI het geheime personage al van tevoren "kende", ook al deelde de code die informatie nooit expliciet. Dit laat een cruciaal aspect van mens versus machine zien:
Het gedrag van een AI moet niet alleen correct zijn, het moet ook eerlijk aanvoelen .
Daarom heb ik de prompts bijgewerkt met botte instructies, zoals: "Je weet NIET welk personage ik heb gekozen" en "Niet vals spelen". Ik heb geleerd dat je bij het bouwen van AI-agenten waarschijnlijk meer tijd moet besteden aan het definiëren van beperkingen dan aan instructies.
De interactie met het model kan nog verder verbeterd worden. Door met een ingebouwd model te werken, verlies je een deel van de kracht en betrouwbaarheid van een enorm server-side model, maar je wint er wel privacy, snelheid en offline mogelijkheden mee. Voor een game als deze was die afweging echt de moeite waard om mee te experimenteren. De toekomst van client-side AI wordt met de dag beter, modellen worden ook kleiner, en ik kan niet wachten om te zien wat we hierna gaan bouwen.
,Gepubliceerd: 10 oktober 2025
Het klassieke bordspel ' Wie is het? ' is een masterclass in deductief redeneren. Elke speler begint met een bord met plaatjes en beperkt, door middel van een reeks ja-/nee-vragen, de mogelijkheden totdat je het geheime personage van je tegenstander met zekerheid kunt identificeren.
Nadat ik een demo van ingebouwde AI had gezien op Google I/O Connect, vroeg ik me af: wat als ik een 'Wie is het?'-spel zou kunnen spelen tegen een AI die in de browser zit? Met client-side AI zouden de foto's lokaal worden geïnterpreteerd, zodat een eigen 'Wie is het?'-spel van vrienden en familie privé en veilig op mijn apparaat zou blijven.
Mijn achtergrond ligt voornamelijk in UI- en UX-ontwikkeling, en ik ben gewend om pixelperfecte ervaringen te bouwen. Ik hoopte dat ik precies dat met mijn interpretatie zou kunnen doen.
Mijn applicatie, AI Guess Who?, is gebouwd met React en gebruikt de Prompt API en een in de browser ingebouwd model om een verrassend capabele tegenstander te creëren. Tijdens dit proces ontdekte ik dat het niet zo eenvoudig is om "pixelperfecte" resultaten te behalen. Maar deze applicatie laat zien hoe AI kan worden gebruikt om doordachte spellogica te ontwikkelen, en hoe belangrijk prompt engineering is om deze logica te verfijnen en de resultaten te behalen die je verwacht.
Lees verder om meer te weten te komen over de ingebouwde AI-integratie, de uitdagingen waar ik voor stond en de oplossingen die ik heb gevonden. Je kunt het spel spelen en de broncode vinden op GitHub .
Game foundation: een React-app
Voordat we naar de AI-implementatie kijken, bekijken we de structuur van de applicatie. Ik heb een standaard React-applicatie gebouwd met TypeScript, met een centraal App.tsx
bestand dat als dirigent van het spel fungeert. Dit bestand bevat:
- Spelstatus : een opsomming die de huidige fase van het spel bijhoudt (zoals
PLAYER_TURN_ASKING
,AI_TURN
,GAME_OVER
). Dit is het belangrijkste onderdeel van de status, omdat het bepaalt wat de interface weergeeft en welke acties beschikbaar zijn voor de speler. - Karakterlijsten : Er zijn meerdere lijsten met de actieve karakters, het geheime karakter van elke speler en welke karakters van het bord zijn verwijderd.
- Gamechat : een lopend logboek met vragen, antwoorden en systeemberichten.
De interface is onderverdeeld in logische componenten:


Naarmate de functies van de game toenamen, nam ook de complexiteit toe. Aanvankelijk werd de hele logica van de game beheerd binnen één grote, aangepaste React-hook , useGameLogic
, maar deze werd al snel te groot om te navigeren en te debuggen. Om de onderhoudbaarheid te verbeteren, heb ik deze hook geherstructureerd in meerdere hooks, elk met één verantwoordelijkheid. Bijvoorbeeld:
-
useGameState
beheert de kernstatus -
usePlayerActions
is voor de beurt van de speler -
useAIActions
is voor de logica van de AI
De belangrijkste useGameLogic
hook fungeert nu als een overzichtelijke composer, waarbij deze kleinere hooks samen worden geplaatst. Deze architectuurwijziging heeft de functionaliteit van de game niet veranderd, maar de codebase is er wel een stuk overzichtelijker door geworden.
Spellogica met de Prompt API
De kern van dit project is het gebruik van de Prompt API.
Ik heb de AI-gamelogica toegevoegd aan builtInAIService.ts
. Dit zijn de belangrijkste verantwoordelijkheden:
- Sta beperkende, binaire antwoorden toe.
- Leer de strategie van het modelspel.
- Leer de modelanalyse.
- Geef het model geheugenverlies.
Beperkende, binaire antwoorden toestaan
Hoe werkt de speler samen met de AI? Wanneer een speler vraagt: "Heeft je personage een hoed?", moet de AI naar de afbeelding van zijn geheime personage "kijken" en een duidelijk antwoord geven.
Mijn eerste pogingen liepen op een mislukking uit. Het antwoord was een alledaags antwoord: "Nee, het personage waar ik aan denk, Isabella, lijkt geen hoed te dragen", in plaats van een binair ja of nee. Aanvankelijk loste ik dit op met een zeer strikte prompt, waarbij ik het model in feite dicteerde om alleen met "Ja" of "Nee" te antwoorden.
Terwijl dit werkte, leerde ik een nog betere manier kennen, namelijk gestructureerde output . Door het JSON-schema aan het model te verstrekken, kon ik een waar/onwaar-antwoord garanderen.
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
Hierdoor kon ik de prompt vereenvoudigen en mijn code de respons betrouwbaar laten verwerken:
JSON.parse(result) ? "Yes" : "No"
Leer de modelspelstrategie
Het model een vraag laten beantwoorden is veel eenvoudiger dan het model zelf vragen te laten stellen. Een goede 'Wie is het?'-speler stelt geen willekeurige vragen. Hij stelt vragen die in één keer de meeste personages elimineren. Een ideale vraag halveert het aantal mogelijke resterende personages door binaire vragen te stellen.
Hoe leer je een model die strategie? Nogmaals, prompt engineering. De prompt voor generateAIQuestion()
is eigenlijk een beknopte les in 'Wie is het?'-speltheorie.
Aanvankelijk vroeg ik het model om "een goede vraag te stellen". De resultaten waren onvoorspelbaar. Om de resultaten te verbeteren, voegde ik negatieve beperkingen toe. De prompt bevat nu instructies die vergelijkbaar zijn met:
- "KRITISCH: Vraag ALLEEN naar bestaande functies"
- KRITISCH: Wees origineel. Herhaal GEEN vraag.
Deze beperkingen beperken de focus van het model en voorkomen dat het irrelevante vragen stelt, wat het een veel leukere tegenstander maakt. Je kunt het volledige promptbestand bekijken op GitHub .
Leer de modelanalyse
Dit was verreweg de moeilijkste en belangrijkste uitdaging. Als het model een vraag stelt, zoals: "Heeft je personage een hoed?", en de speler antwoordt nee, hoe weet het model dan welke personages op zijn bord zijn uitgeschakeld?
Het model zou iedereen met een hoed moeten elimineren. Mijn eerste pogingen zaten vol met logische fouten, en soms elimineerde het model de verkeerde tekens of helemaal geen tekens. En wat is een "hoed"? Telt een "muts" ook als een "hoed"? Dit is, laten we eerlijk zijn, ook iets wat kan gebeuren in een menselijk debat. En natuurlijk gebeuren er wel eens algemene fouten. Haar kan er vanuit een AI-perspectief uitzien als een hoed.
Ik heb de architectuur opnieuw ontworpen om perceptie te scheiden van code-deductie:
AI is verantwoordelijk voor visuele analyse . Modellen blinken uit in visuele analyse. Ik heb het model geïnstrueerd om zijn vraag en een gedetailleerde analyse te retourneren in een strikt JSON-schema. Het model analyseert elk personage op zijn bord en beantwoordt de vraag: "Heeft dit personage deze eigenschap?" Het model retourneert een gestructureerd JSON-object:
{ "character_id": "...", "has_feature": true }
Ook hier zijn gestructureerde gegevens de sleutel tot een succesvol resultaat.
De gamecode gebruikt de analyse om de uiteindelijke beslissing te nemen . De applicatiecode controleert het antwoord van de speler ("Ja" of "Nee") en herhaalt de analyse van de AI. Als de speler "Nee" antwoordt, weet de code dat elk teken waarvoor
has_feature
true
is, moet worden verwijderd.
Ik ontdekte dat deze taakverdeling essentieel is voor het bouwen van betrouwbare AI-applicaties. Gebruik de AI vanwege de analytische mogelijkheden en laat binaire beslissingen over aan je applicatiecode.
Om de perceptie van het model te controleren, maakte ik een visualisatie van deze analyse. Dit maakte het gemakkelijker om te bevestigen of de perceptie van het model correct was.
Snelle engineering
Maar zelfs met deze scheiding merkte ik dat de perceptie van het model nog steeds gebrekkig kon zijn. Het kon verkeerd inschatten of een personage een bril droeg, wat leidde tot een frustrerende, onterechte eliminatie. Om dit te verhelpen, experimenteerde ik met een tweestaps-proces: de AI stelde zijn vraag. Na ontvangst van het antwoord van de speler voerde hij een tweede, frisse analyse uit met het antwoord als context. De theorie was dat een tweede blik fouten uit de eerste blik zou kunnen opsporen.
Dit is hoe de stroom zou hebben gewerkt:
- AI-beurt (API-aanroep 1) : AI vraagt: "Heeft je personage een baard?"
- De beurt aan de speler : De speler kijkt naar zijn geheime personage, die gladgeschoren is, en antwoordt: "Nee."
- AI-beurt (API-aanroep 2) : De AI vraagt zichzelf om nogmaals naar alle overgebleven personages te kijken en te bepalen welke personages op basis van het antwoord van de speler moeten worden geëlimineerd.
In stap twee kan het model een personage met een lichte stoppelbaard nog steeds verkeerd interpreteren als "geen baard hebbend" en deze niet elimineren, ondanks dat de gebruiker dat wel verwachtte. De kernfout in de perceptie werd niet hersteld en de extra stap vertraagde de resultaten alleen maar. Wanneer we tegen een menselijke tegenstander spelen, kunnen we hierover een overeenkomst of verduidelijking afspreken; in de huidige opstelling met onze AI-tegenstander is dit niet het geval.
Dit proces zorgde voor extra latentie bij een tweede API-aanroep, zonder dat de nauwkeurigheid significant verbeterde. Als het model de eerste keer fout was, was het vaak de tweede keer ook fout. Ik heb de prompt slechts één keer teruggezet naar 'review'.
Verbeteren in plaats van meer analyses toevoegen
Ik baseerde mij op een UX-principe: de oplossing was niet meer analyse, maar betere analyse.
Ik heb flink geïnvesteerd in het verfijnen van de prompt en expliciete instructies toegevoegd om het model te laten controleren en zich te concentreren op specifieke kenmerken. Dit bleek een effectievere strategie om de nauwkeurigheid te verbeteren. Zo werkt de huidige, betrouwbaardere flow:
AI-beurt (API-aanroep) : het model wordt gevraagd om zowel de vraag als de interne analyse tegelijkertijd te genereren en één enkel JSON-object te retourneren.
- Vraag : "Draagt jouw personage een bril?"
- Analyse (gegevens) :
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
Beurt van de speler : Het geheime karakter van de speler is Alex (geen bril), dus hij antwoordt: "Nee."
Einde van de ronde : de JavaScript-code van de applicatie neemt het over. Deze hoeft de AI verder niets te vragen. De analysegegevens van stap 1 worden doorlopen.
- De speler zei "Nee."
- De code zoekt naar elk teken waarbij
has_feature
true is. - Het draait Brad en Gina om. De logica is deterministisch en direct.
Dit experiment was cruciaal, maar vergde veel vallen en opstaan. Ik had geen idee of het beter zou worden. Soms werd het zelfs erger. Bepalen hoe je de meest consistente resultaten krijgt, is geen exacte wetenschap (nog niet, als dat ooit gebeurt...).
Maar na een paar rondes met mijn nieuwe AI-tegenstander, deed zich een fantastisch nieuw probleem voor: een patstelling.
Ontsnap aan impasse
Als er nog maar twee of drie zeer vergelijkbare personages overbleven, raakte het model in een lus. Het stelde dan een vraag over een kenmerk dat ze allemaal deelden, zoals: "Draagt jouw personage een hoed?"
Mijn code zou dit correct identificeren als een verspilde beurt, en de AI zou een andere, even brede functie proberen die alle personages ook deelden, zoals: "Draagt je personage een bril?"
Ik heb de prompt verbeterd met een nieuwe regel: als een poging om een vraag te genereren mislukt en er nog maar drie of minder tekens over zijn, verandert de strategie.
De nieuwe instructie is expliciet: "In plaats van een algemeen kenmerk, moet je vragen naar een specifieker, uniek of gecombineerd visueel kenmerk om een verschil te vinden." Zo wordt bijvoorbeeld in plaats van de vraag of het personage een hoed draagt, gevraagd of hij of zij een honkbalpet draagt.
Hierdoor moet het model veel beter naar de beelden kijken om dat ene kleine detail te vinden dat uiteindelijk tot een doorbraak kan leiden. Hierdoor werkt de strategie in de late game in de meeste gevallen iets beter.
Geef het model geheugenverlies
De grootste kracht van een taalmodel is zijn geheugen. Maar in dit spel werd die grootste kracht een zwakte. Toen ik een tweede spel begon, stelde het verwarrende of irrelevante vragen. Natuurlijk bewaarde mijn slimme AI-tegenstander de volledige chatgeschiedenis van het vorige spel. Hij probeerde twee (of zelfs meer) spellen tegelijk te begrijpen.
In plaats van dezelfde AI-sessie opnieuw te gebruiken, vernietig ik deze nu expliciet aan het einde van elk spel, waardoor de AI feitelijk geheugenverlies krijgt.
Wanneer je op Opnieuw afspelen klikt, reset de functie startNewGameSession()
het bord en creëert een gloednieuwe AI-sessie. Dit was een interessante les in het beheren van de sessiestatus, niet alleen in de app, maar ook binnen het AI-model zelf.
Extraatjes: aangepaste spellen en spraakinvoer
Om de ervaring nog aantrekkelijker te maken, heb ik twee extra functies toegevoegd:
Aangepaste tekens : Met
getUserMedia()
kunnen spelers hun camera gebruiken om hun eigen set van 5 tekens te maken. Ik heb IndexedDB gebruikt om de tekens op te slaan, een browserdatabase die perfect is voor het opslaan van binaire gegevens zoals afbeeldingsblobs. Wanneer je een aangepaste set maakt, wordt deze opgeslagen in je browser en verschijnt er een afspeeloptie in het hoofdmenu.Spraakinvoer : Het client-side model is multimodaal . Het kan tekst, afbeeldingen en ook audio verwerken. Door de MediaRecorder API te gebruiken om microfooninvoer vast te leggen, kon ik de resulterende audioblob naar het model sturen met de prompt: "Transcribeer de volgende audio...". Dit voegt een leuke manier van spelen toe (en een leuke manier om te zien hoe het mijn Vlaamse accent interpreteert). Ik heb dit vooral gemaakt om de veelzijdigheid van deze nieuwe webfunctie te laten zien, maar eerlijk gezegd was ik het zat om steeds maar weer vragen te typen.
Laatste gedachten
Het bouwen van "AI Raad eens Wie?" was absoluut een uitdaging. Maar met een beetje hulp van het lezen van documentatie en wat AI om AI te debuggen (ja... dat heb ik gedaan), bleek het een leuk experiment. Het benadrukte de immense potentie van het draaien van een model in de browser voor het creëren van een privé, snelle ervaring zonder internet. Dit is nog steeds een experiment, en soms speelt de tegenstander gewoon niet perfect. Het is niet pixelperfect of logisch perfect. Met generatieve AI zijn de resultaten afhankelijk van het model.
In plaats van te streven naar perfectie, streef ik ernaar het resultaat te verbeteren.
Dit project onderstreepte ook de constante uitdagingen van prompt engineering. Die prompting werd echt een belangrijk onderdeel ervan, en niet altijd het leukste. Maar de belangrijkste les die ik leerde, was het ontwerpen van de applicatie om perceptie en deductie te scheiden, en zo de mogelijkheden van AI en code te scheiden. Zelfs met die scheiding ontdekte ik dat de AI nog steeds (voor een mens) voor de hand liggende fouten kon maken, zoals tatoeages verwarren met make-up of de draad kwijtraken van wiens geheime personage er werd gesproken.
De oplossing was om de prompts steeds duidelijker te maken, door instructies toe te voegen die voor een mens vanzelfsprekend zijn, maar essentieel voor het model.
Soms voelde de game oneerlijk. Soms had ik het gevoel dat de AI het geheime personage al van tevoren "kende", ook al deelde de code die informatie nooit expliciet. Dit laat een cruciaal aspect van mens versus machine zien:
Het gedrag van een AI moet niet alleen correct zijn, het moet ook eerlijk aanvoelen .
Daarom heb ik de prompts bijgewerkt met botte instructies, zoals: "Je weet NIET welk personage ik heb gekozen" en "Niet vals spelen". Ik heb geleerd dat je bij het bouwen van AI-agenten waarschijnlijk meer tijd moet besteden aan het definiëren van beperkingen dan aan instructies.
De interactie met het model kan nog verder verbeterd worden. Door met een ingebouwd model te werken, verlies je een deel van de kracht en betrouwbaarheid van een enorm server-side model, maar je wint er wel privacy, snelheid en offline mogelijkheden mee. Voor een game als deze was die afweging echt de moeite waard om mee te experimenteren. De toekomst van client-side AI wordt met de dag beter, modellen worden ook kleiner, en ik kan niet wachten om te zien wat we hierna gaan bouwen.
,Gepubliceerd: 10 oktober 2025
Het klassieke bordspel ' Wie is het? ' is een masterclass in deductief redeneren. Elke speler begint met een bord met plaatjes en beperkt, door middel van een reeks ja-/nee-vragen, de mogelijkheden totdat je het geheime personage van je tegenstander met zekerheid kunt identificeren.
Nadat ik een demo van ingebouwde AI had gezien op Google I/O Connect, vroeg ik me af: wat als ik een 'Wie is het?'-spel zou kunnen spelen tegen een AI die in de browser zit? Met client-side AI zouden de foto's lokaal worden geïnterpreteerd, zodat een eigen 'Wie is het?'-spel van vrienden en familie privé en veilig op mijn apparaat zou blijven.
Mijn achtergrond ligt voornamelijk in UI- en UX-ontwikkeling, en ik ben gewend om pixelperfecte ervaringen te bouwen. Ik hoopte dat ik precies dat met mijn interpretatie zou kunnen doen.
Mijn applicatie, AI Guess Who?, is gebouwd met React en gebruikt de Prompt API en een in de browser ingebouwd model om een verrassend capabele tegenstander te creëren. Tijdens dit proces ontdekte ik dat het niet zo eenvoudig is om "pixelperfecte" resultaten te behalen. Maar deze applicatie laat zien hoe AI kan worden gebruikt om doordachte spellogica te ontwikkelen, en hoe belangrijk prompt engineering is om deze logica te verfijnen en de resultaten te behalen die je verwacht.
Lees verder om meer te weten te komen over de ingebouwde AI-integratie, de uitdagingen waar ik voor stond en de oplossingen die ik heb gevonden. Je kunt het spel spelen en de broncode vinden op GitHub .
Game foundation: een React-app
Voordat we naar de AI-implementatie kijken, bekijken we de structuur van de applicatie. Ik heb een standaard React-applicatie gebouwd met TypeScript, met een centraal App.tsx
bestand dat als dirigent van het spel fungeert. Dit bestand bevat:
- Game state : An enum that tracks the current phase of the game (such as
PLAYER_TURN_ASKING
,AI_TURN
,GAME_OVER
). This is the most important piece of state, as it dictates what the interface displays and what actions are available to the player. - Character lists : There are multiple lists that designate the active characters, each players' secret character, and which characters have been eliminated from the board.
- Game chat : A running log of questions, answers, and system messages.
The interface is broken down into logical components:


As the game's features grew, so did its complexity. Initially, the entire game's logic was managed within a single, large custom React hook , useGameLogic
, but it quickly became too large to navigate and debug. To improve maintainability, I refactored this hook into multiple hooks, each with a single responsibility. For example:
-
useGameState
manages the core state -
usePlayerActions
is for the player's turn -
useAIActions
is for the AI's logic
The main useGameLogic
hook now acts as a clean composer, placing these smaller hooks together. This architectural change didn't alter the game's functionality, but it made the codebase a whole lot cleaner.
Game logic with the Prompt API
The core of this project is the use of the Prompt API.
I added the AI game logic to builtInAIService.ts
. These are its key responsibilities:
- Allow restrictive, binary answers.
- Teach the model game strategy.
- Teach the model analysis.
- Give the model amnesia.
Allow restrictive, binary answers
How does the player interact with the AI? When a player asks, "Does your character have a hat?", the AI needs to "look" at its secret character's image and give a clear answer.
My first attempts were a mess. The response was conversational: "No, the character I'm thinking of, Isabella, does not appear to be wearing a hat," instead of offering a binary yes or no. Initially, I solved this with a very strict prompt, essentially dictating to the model to only respond with "Yes" or "No".
While this worked, I learned of an even better way using structured output . By providing the JSON Schema to the model, I could guarantee a true or false response.
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
This allowed me to simplify the prompt and let my code reliably handle the response:
JSON.parse(result) ? "Yes" : "No"
Teach the model game strategy
Telling the model to answer a question is much simpler than having the model initiate and ask questions. A good Guess Who? player doesn't ask random questions. They ask questions that eliminate the most characters at once. An ideal question reduces the possible remaining characters in half using binary questions.
How do you teach a model that strategy? Again, prompt engineering. The prompt for generateAIQuestion()
is actually a concise lesson in Guess Who? game theory.
Initially, I asked the model to "ask a good question." The results were unpredictable. To improve the results, I added negative constraints. The prompt now includes instructions similar to:
- "CRITICAL: Ask about existing features ONLY"
- "CRITICAL: Be original. Do NOT repeat a question".
These constraints narrow the model's focus, prevent it from asking irrelevant questions, which make it a much more enjoyable opponent. You can review the full prompt file on GitHub .
Teach the model analysis
This was, by far, the most difficult and important challenge. When the model asks a question, such as, "Does your character have a hat," and the player responds no, how does the model know what characters on their board are eliminated?
The model should eliminate everyone with a hat. My early attempts were plagued with logical errors, and sometimes the model eliminated the wrong characters or no characters. Also, what is a "hat"? Does a "beanie" count as a "hat"? This is, let's be honest, also something that can happen in a human debate. And of course, general mistakes happen. Hair can look like a hat from an AI perspective.
I redesigned the architecture to separate perception from code deduction:
AI is responsible for visual analysis . Models excel at visual analysis. I instructed the model to return its question and a detailed analysis in a strict JSON schema. The model analyzes each character on its board and answers the question, "Does this character have this feature?" The model returns a structured JSON object:
{ "character_id": "...", "has_feature": true }
Once again, structured data is key to a successful outcome.
Game code uses the analysis to make the final decision . The application code checks the player's answer ("Yes" or "No") and iterates through the AI's analysis. If the player said "No," the code knows to eliminate every character where
has_feature
istrue
.
I found this division of labor is key to building reliable AI applications. Use the AI for its analytic capabilities, and leave binary decisions to your application code.
To check the model's perception, I built a visualization of this analysis. This made it easier to confirm if the model's perception was correct.
Prompt engineering
However, even with this separation, I noticed the model's perception could still be flawed. It might misjudge whether a character wore glasses, leading to a frustrating, incorrect elimination. To combat this, I experimented with a two-step process: the AI would ask its question. After receiving the player's answer, it would perform a second, fresh analysis with the answer as context. The theory was that a second look might catch errors from the first.
Here's how that flow would have worked:
- AI turn (API call 1) : AI asks, "Does your character have a beard?"
- Player's turn : The player looks at their secret character, who is clean-shaven, and answers, "No."
- AI turn (API call 2) : The AI effectively asks itself to look at all of its remaining characters, again, and determine which ones to eliminate based on the player's answer.
In step two, the model might still misperceive a character with a light stubble as "not having a beard" and fail to eliminate them, even though the user expected it to. The core perception error wasn't fixed, and the extra step just delayed the results. When playing against a human opponent, we can specify an agreement or clarification on this; in the current setup with our AI opponent, this isn't the case.
This process added latency from a second API call, without gaining a significant boost in accuracy. If the model was wrong the first time, it was often wrong the second time, too. I reverted the prompt to review just once.
Improve instead of adding more analysis
I relied on a UX principle: The solution wasn't more analysis, but better analysis.
I invested heavily in refining the prompt, adding explicit instructions for the model to double-check its work and focus on distinct features, which proved to be a more effective strategy for improving accuracy. Here's how the current, more reliable flow works:
AI turn (API call) : The model is prompted to generate both its question and its internal analysis at the same time, returning a single JSON object.
- Question : "Does your character wear glasses?"
- Analysis (data) :
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
Player's turn : The player's secret character is Alex (no glasses), so they answer, "No."
Round ends : The application's JavaScript code takes over. It doesn't need to ask the AI anything else. It iterates through the analysis data from step 1.
- The player said "No."
- The code looks for every character where
has_feature
is true. - It flips down Brad and Gina. The logic is deterministic and instant.
This experimentation was crucial, but required a lot of trial and error. I had no idea if it was going to get better. Sometimes, it got even worse. Determining how to get the most consistent results isn't an exact science (yet, if ever...).
But after a few rounds with my new AI opponent, a fantastic new issue appeared: a stalemate.
Escape deadlock
When only two or three very similar characters remained, the model would get stuck in a loop. It would ask a question about a feature they all shared, such as, "Does your character wear a hat?"
My code would correctly identify this as a wasted turn, and the AI would try another, equally broad feature the characters also all shared, such as, "Does your character wear glasses?"
I enhanced the prompt with a new rule: if a question generation attempt fails and there are three or fewer characters left, the strategy changes.
The new instruction is explicit: "Instead of a broad feature, you must ask about a more specific, unique, or combined visual feature to find a difference." For example, instead of asking if the character wears a hat, it's prompted to ask if they're wearing a baseball cap.
This forces the model to look much closer at the images to find the one small detail that can finally lead to a breakthrough, making its late-game strategy work a little better, most of the time.
Give the model amnesia
A language model's greatest strength is its memory. But in this game, its greatest strength became a weakness. When I started a second game, it would ask confusing or irrelevant questions. Of course, my smart AI opponent was retaining the entire chat history from the previous game. It was trying to make sense of two (or even more) games at once.
Instead of reusing the same AI session, I now explicitly destroy it at the end of each game, essentially giving the AI amnesia.
When you click Play Again , the startNewGameSession()
function resets the board and creates a brand new AI session. This was an interesting lesson in managing session state not just in the app, but within the AI model itself.
Bells and whistles: Custom games and voice input
To make the experience more engaging, I added two extra features:
Custom characters : With
getUserMedia()
, players can use their camera to create their own 5-character set. I used IndexedDB to save the characters, a browser database perfect for storing binary data like image blobs. When you create a custom set, it's saved to your browser, and a replay option appears in the main menu.Voice input : The client-side model is multi-modal . It can handle text, images, and also audio. Using the MediaRecorder API to capture microphone input, I could feed the resulting audio blob to the model with a prompt: "Transcribe the following audio...". This adds a fun way to play (and a fun way to see how it interprets my Flemish accent). I created this mostly to show the versatility of this new web capability, but truth be told, I was sick of typing questions over and over again.
Laatste gedachten
Building "AI Guess Who?" was definitely a challenge. But with a bit of help from reading docs and some AI to debug AI (yeah... I did that), it turned out to be a fun experiment. It highlighted the immense potential of running a model in the browser for creating a private, fast, no-internet-required experience. This is still an experiment, and sometimes the opponent just doesn't play perfectly. It's not pixel-perfect or logic-perfect. With generative AI, the results are model-dependent.
Instead of striving for perfection, I'll aim for improving the outcome.
This project also underscored the constant challenges of prompt engineering. That prompting really became a huge part of it, and not always the most fun part. But the most critical lesson I learned was architecting the application to separate perception from deduction, dividing capabilities of AI and code. Even with that separation, I found that the AI could still make (to a human) obvious mistakes, like confusing tattoos for make-up or losing track of whose secret character was being discussed.
Each time, the solution was to make the prompts even more explicit, adding instructions that feel obvious to a human but are essential for the model.
Sometimes, the game felt unfair. Occasionally, I felt like the AI "knew" the secret character ahead of time, even though the code never explicitly shared that information. This shows a crucial part of human versus machine:
An AI's behavior doesn't just need to be correct; it needs to feel fair.
This is why I updated the prompts with blunt instructions, such as, "You do NOT know which character I have picked," and "No cheating." I learned that when building AI agents, you should spend time defining limitations, probably even more than instructions.
The interaction with the model could continue to be improved. By working with a built-in model, you lose some of the power and reliability of a massive server-side model, but you gain privacy, speed, and offline capability. For a game like this, that tradeoff was really worth experimenting with. The future of client-side AI is getting better by the day, models are getting smaller as well, and I can't wait to see what we'll be able to build next.
,Published: October 10, 2025
The classic board game, Guess Who? , is a masterclass in deductive reasoning. Each player starts with a board of faces and, through a series of yes or no questions, narrows down the possibilities until you can confidently identify your opponent's secret character.
After seeing a demo of built-in AI at Google I/O Connect, I wondered: what if I could play a Guess Who? game against AI that lives in the browser? With client-side AI, the photos would be interpreted locally, so a custom Guess Who? of friends and family would remain private and secure on my device.
My background is primarily in UI and UX development, and I'm used to building pixel-perfect experiences. I hoped I could do exactly that with my interpretation.
My application, AI Guess Who? , is built with React and uses the Prompt API and a browser built-in model to create a surprisingly capable opponent. In this process, I discovered it's not so simple to get "pixel-perfect" results. But, this application demonstrates how AI can be used to build thoughtful game logic, and the importance of prompt engineering to refine this logic and get the outcomes you expect.
Keep reading to learn about the built-in AI integration, challenges I faced, and the solutions I landed on. You can play the game and find the source code on GitHub .
Game foundation: A React app
Before you look at the AI implementation, we'll review the application's structure. I built a standard React application with TypeScript, with a central App.tsx
file to act as the game's conductor. This file holds:
- Game state : An enum that tracks the current phase of the game (such as
PLAYER_TURN_ASKING
,AI_TURN
,GAME_OVER
). This is the most important piece of state, as it dictates what the interface displays and what actions are available to the player. - Character lists : There are multiple lists that designate the active characters, each players' secret character, and which characters have been eliminated from the board.
- Game chat : A running log of questions, answers, and system messages.
The interface is broken down into logical components:


As the game's features grew, so did its complexity. Initially, the entire game's logic was managed within a single, large custom React hook , useGameLogic
, but it quickly became too large to navigate and debug. To improve maintainability, I refactored this hook into multiple hooks, each with a single responsibility. For example:
-
useGameState
manages the core state -
usePlayerActions
is for the player's turn -
useAIActions
is for the AI's logic
The main useGameLogic
hook now acts as a clean composer, placing these smaller hooks together. This architectural change didn't alter the game's functionality, but it made the codebase a whole lot cleaner.
Game logic with the Prompt API
The core of this project is the use of the Prompt API.
I added the AI game logic to builtInAIService.ts
. These are its key responsibilities:
- Allow restrictive, binary answers.
- Teach the model game strategy.
- Teach the model analysis.
- Give the model amnesia.
Allow restrictive, binary answers
How does the player interact with the AI? When a player asks, "Does your character have a hat?", the AI needs to "look" at its secret character's image and give a clear answer.
My first attempts were a mess. The response was conversational: "No, the character I'm thinking of, Isabella, does not appear to be wearing a hat," instead of offering a binary yes or no. Initially, I solved this with a very strict prompt, essentially dictating to the model to only respond with "Yes" or "No".
While this worked, I learned of an even better way using structured output . By providing the JSON Schema to the model, I could guarantee a true or false response.
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
This allowed me to simplify the prompt and let my code reliably handle the response:
JSON.parse(result) ? "Yes" : "No"
Teach the model game strategy
Telling the model to answer a question is much simpler than having the model initiate and ask questions. A good Guess Who? player doesn't ask random questions. They ask questions that eliminate the most characters at once. An ideal question reduces the possible remaining characters in half using binary questions.
How do you teach a model that strategy? Again, prompt engineering. The prompt for generateAIQuestion()
is actually a concise lesson in Guess Who? game theory.
Initially, I asked the model to "ask a good question." The results were unpredictable. To improve the results, I added negative constraints. The prompt now includes instructions similar to:
- "CRITICAL: Ask about existing features ONLY"
- "CRITICAL: Be original. Do NOT repeat a question".
These constraints narrow the model's focus, prevent it from asking irrelevant questions, which make it a much more enjoyable opponent. You can review the full prompt file on GitHub .
Teach the model analysis
This was, by far, the most difficult and important challenge. When the model asks a question, such as, "Does your character have a hat," and the player responds no, how does the model know what characters on their board are eliminated?
The model should eliminate everyone with a hat. My early attempts were plagued with logical errors, and sometimes the model eliminated the wrong characters or no characters. Also, what is a "hat"? Does a "beanie" count as a "hat"? This is, let's be honest, also something that can happen in a human debate. And of course, general mistakes happen. Hair can look like a hat from an AI perspective.
I redesigned the architecture to separate perception from code deduction:
AI is responsible for visual analysis . Models excel at visual analysis. I instructed the model to return its question and a detailed analysis in a strict JSON schema. The model analyzes each character on its board and answers the question, "Does this character have this feature?" The model returns a structured JSON object:
{ "character_id": "...", "has_feature": true }
Once again, structured data is key to a successful outcome.
Game code uses the analysis to make the final decision . The application code checks the player's answer ("Yes" or "No") and iterates through the AI's analysis. If the player said "No," the code knows to eliminate every character where
has_feature
istrue
.
I found this division of labor is key to building reliable AI applications. Use the AI for its analytic capabilities, and leave binary decisions to your application code.
To check the model's perception, I built a visualization of this analysis. This made it easier to confirm if the model's perception was correct.
Prompt engineering
However, even with this separation, I noticed the model's perception could still be flawed. It might misjudge whether a character wore glasses, leading to a frustrating, incorrect elimination. To combat this, I experimented with a two-step process: the AI would ask its question. After receiving the player's answer, it would perform a second, fresh analysis with the answer as context. The theory was that a second look might catch errors from the first.
Here's how that flow would have worked:
- AI turn (API call 1) : AI asks, "Does your character have a beard?"
- Player's turn : The player looks at their secret character, who is clean-shaven, and answers, "No."
- AI turn (API call 2) : The AI effectively asks itself to look at all of its remaining characters, again, and determine which ones to eliminate based on the player's answer.
In step two, the model might still misperceive a character with a light stubble as "not having a beard" and fail to eliminate them, even though the user expected it to. The core perception error wasn't fixed, and the extra step just delayed the results. When playing against a human opponent, we can specify an agreement or clarification on this; in the current setup with our AI opponent, this isn't the case.
This process added latency from a second API call, without gaining a significant boost in accuracy. If the model was wrong the first time, it was often wrong the second time, too. I reverted the prompt to review just once.
Improve instead of adding more analysis
I relied on a UX principle: The solution wasn't more analysis, but better analysis.
I invested heavily in refining the prompt, adding explicit instructions for the model to double-check its work and focus on distinct features, which proved to be a more effective strategy for improving accuracy. Here's how the current, more reliable flow works:
AI turn (API call) : The model is prompted to generate both its question and its internal analysis at the same time, returning a single JSON object.
- Question : "Does your character wear glasses?"
- Analysis (data) :
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
Player's turn : The player's secret character is Alex (no glasses), so they answer, "No."
Round ends : The application's JavaScript code takes over. It doesn't need to ask the AI anything else. It iterates through the analysis data from step 1.
- The player said "No."
- The code looks for every character where
has_feature
is true. - It flips down Brad and Gina. The logic is deterministic and instant.
This experimentation was crucial, but required a lot of trial and error. I had no idea if it was going to get better. Sometimes, it got even worse. Determining how to get the most consistent results isn't an exact science (yet, if ever...).
But after a few rounds with my new AI opponent, a fantastic new issue appeared: a stalemate.
Escape deadlock
When only two or three very similar characters remained, the model would get stuck in a loop. It would ask a question about a feature they all shared, such as, "Does your character wear a hat?"
My code would correctly identify this as a wasted turn, and the AI would try another, equally broad feature the characters also all shared, such as, "Does your character wear glasses?"
I enhanced the prompt with a new rule: if a question generation attempt fails and there are three or fewer characters left, the strategy changes.
The new instruction is explicit: "Instead of a broad feature, you must ask about a more specific, unique, or combined visual feature to find a difference." For example, instead of asking if the character wears a hat, it's prompted to ask if they're wearing a baseball cap.
This forces the model to look much closer at the images to find the one small detail that can finally lead to a breakthrough, making its late-game strategy work a little better, most of the time.
Give the model amnesia
A language model's greatest strength is its memory. But in this game, its greatest strength became a weakness. When I started a second game, it would ask confusing or irrelevant questions. Of course, my smart AI opponent was retaining the entire chat history from the previous game. It was trying to make sense of two (or even more) games at once.
Instead of reusing the same AI session, I now explicitly destroy it at the end of each game, essentially giving the AI amnesia.
When you click Play Again , the startNewGameSession()
function resets the board and creates a brand new AI session. This was an interesting lesson in managing session state not just in the app, but within the AI model itself.
Bells and whistles: Custom games and voice input
To make the experience more engaging, I added two extra features:
Custom characters : With
getUserMedia()
, players can use their camera to create their own 5-character set. I used IndexedDB to save the characters, a browser database perfect for storing binary data like image blobs. When you create a custom set, it's saved to your browser, and a replay option appears in the main menu.Voice input : The client-side model is multi-modal . It can handle text, images, and also audio. Using the MediaRecorder API to capture microphone input, I could feed the resulting audio blob to the model with a prompt: "Transcribe the following audio...". This adds a fun way to play (and a fun way to see how it interprets my Flemish accent). I created this mostly to show the versatility of this new web capability, but truth be told, I was sick of typing questions over and over again.
Laatste gedachten
Building "AI Guess Who?" was definitely a challenge. But with a bit of help from reading docs and some AI to debug AI (yeah... I did that), it turned out to be a fun experiment. It highlighted the immense potential of running a model in the browser for creating a private, fast, no-internet-required experience. This is still an experiment, and sometimes the opponent just doesn't play perfectly. It's not pixel-perfect or logic-perfect. With generative AI, the results are model-dependent.
Instead of striving for perfection, I'll aim for improving the outcome.
This project also underscored the constant challenges of prompt engineering. That prompting really became a huge part of it, and not always the most fun part. But the most critical lesson I learned was architecting the application to separate perception from deduction, dividing capabilities of AI and code. Even with that separation, I found that the AI could still make (to a human) obvious mistakes, like confusing tattoos for make-up or losing track of whose secret character was being discussed.
Each time, the solution was to make the prompts even more explicit, adding instructions that feel obvious to a human but are essential for the model.
Sometimes, the game felt unfair. Occasionally, I felt like the AI "knew" the secret character ahead of time, even though the code never explicitly shared that information. This shows a crucial part of human versus machine:
An AI's behavior doesn't just need to be correct; it needs to feel fair.
This is why I updated the prompts with blunt instructions, such as, "You do NOT know which character I have picked," and "No cheating." I learned that when building AI agents, you should spend time defining limitations, probably even more than instructions.
The interaction with the model could continue to be improved. By working with a built-in model, you lose some of the power and reliability of a massive server-side model, but you gain privacy, speed, and offline capability. For a game like this, that tradeoff was really worth experimenting with. The future of client-side AI is getting better by the day, models are getting smaller as well, and I can't wait to see what we'll be able to build next.