NiPtAIdea: alucinacións, aleatoriedad falsa e un anfitrión de IA sarcástico
NiPtAIdea é un xogo de adiviñanza onde unha IA escolle un concepto secreto — unha persoa, lugar, animal, obra ou concepto abstracto — e tes 15 preguntas para adiviñalo. A IA responde con Si, Non, Frío, Tibio ou Quente. Tamén ten personalidade condescendente e comeza a insultarte se quedas calado demasiado tempo.
A mecánica do xogo é sinxela. Conseguir que a IA se comportase como un anfitrión consistente — e lixeiramente antipático — foi a parte interesante.
Problema 1: os modelos baratos esquezan o seu propio segredo
A primeira versión usaba un modelo pequeno e barato. Baixa latencia, baixo custo — é só un xogo de adiviñanzas, que difícil pode ser?
Bastante. Os modelos pequenos baixo un system prompt restritivo tenden a derivar. Tras varios quendas, o modelo comezaba a contradecir as súas propias respostas anteriores — xogando efectivamente cun concepto diferente ao que escollerá ao principio. Desde o punto de vista do xogador, o xogo vólvese irresoluble. A IA non fai trampa a propósito; simplemente perdeu o fío do que decidiu.
O fallo tiña esta pinta:
Xogador: Está vivo?
IA: Non.
[6 preguntas despois]
Xogador: É un animal?
IA: Tibio! Estaste achegando.
O modelo non cambiara o concepto — esquecérao. Cun modelo capaz isto é raro; cun pequeno é o suficientemente frecuente como para facer o xogo inxogable. A solución foi migrar a Gemini Flash 3 (vía OpenRouter) e volver a enunciar o concepto explicitamente ao inicio de cada batch de mensaxes enviado ao modelo — non só na inicialización da partida. A inferencia sen estado obriga a re-anclar en cada chamada.
Problema 2: a IA sempre elixía can, coche e mazá
Unha vez que o xogo era consistente, apareceu outro problema: a IA seguía elixindo os mesmos conceptos. Can. Coche. Mazá. Cadeira. Piano. Sempre.
Non é un bug — é como funcionan os modelos de linguaxe. Xeran o token máis probable e, cando se lles pide que “escollan un concepto ao azar”, as respostas máis probables son os substantivos máis comúns nos datos de adestramento. Sen ningún empuxe, o modelo converxe sempre no mesmo pequeno conxunto.
Dúas cousas arranxárono:
Unha semente no system prompt. Cada partida xera un número aleatorio no servidor e inxéctao nas instrucións:
const seed = Math.floor(Math.random() * 100000);
const systemPrompt = `
Es o anfitrión dun xogo de adiviñanzas. Semente: ${seed}.
Usa esta semente para variar a túa elección de concepto. Escolle algo específico e
inesperado — evita palabras xenéricas como "can", "coche", "mazá", "cadeira".
...
`;
A semente non funciona como un RNG — o modelo non computa con ela. Pero desprace a ventá de contexto o suficiente para que o modelo mostre dunha rexión diferente da súa distribución. A diversidade de conceptos mellorou notablemente.
Unha lista de conceptos xa vistos desde localStorage. Os últimos 20 conceptos que viu o xogador gárdanse en localStorage e envíanse ao endpoint /api/game/init en cada nova partida. O system prompt inclúeos explicitamente como conceptos a evitar. Isto significa que un xogador que xoga repetidamente obterá variedade entre sesións, non só dentro dunha.
// cliente — antes de comezar unha nova partida
const seen = JSON.parse(localStorage.getItem('seenConcepts') ?? '[]');
const { token } = await fetch('/api/game/init', {
method: 'POST',
body: JSON.stringify({ avoidConcepts: seen }),
}).then(r => r.json());
Combinados, a semente e a lista de evitación fixeron que a elección de concepto se sentise xenuinamente variada.
Outros detalles de implementación que vale a pena mencionar
Tolerancia a erros tipográficos. As respostas do xogador valídanse con distancia de Levenshtein (≤ 2) contra o concepto real, polo que elefanto segue sendo correcto se o concepto é elefante. Sen isto, o xogo séntese inxustamente punitivo.
Cifrado do concepto. O concepto cífrase con AES-GCM antes de envialo ao cliente como token opaco. Isto evita que os xogadores o lean na pestana Network das DevTools. A clave derívase dunha variable de contorno GAME_SECRET.
Taunts automáticos. Se o xogador está inactivo 60, 120, 180 ou 240 segundos, a IA inxecta automaticamente unha mensaxe de burla. Esta foi a funcionalidade máis divertida de axustar — as primeiras versións eran demasiado agresivas e resultaban molestas en lugar de graciosas.
Stack
- Next.js 16 App Router — Server Actions, respostas en streaming
- Gemini Flash 3 vía OpenRouter — suficientemente capaz para gardar un segredo, suficientemente rápido para un xogo
- Vercel AI SDK v6 —
useChatestreamTextpara o bucle de conversación - SQLite /
better-sqlite3— persistencia de sesións e leaderboard top 10 - Docker + Coolify — autoaloxado no meu VPS, ficheiro SQLite nun volume persistente
A lección real
Construír un xogo sobre un modelo de linguaxe significa que o modelo é a túa lóxica de xogo. O prompt engineering non é decoración — é onde viven os bugs. Os dous problemas principais aquí (alucinación e aleatoriedad falsa) viñan da mesma raíz: subestimar canto necesita que lle digan explicitamente, en cada chamada, o que se supón que ten que estar facendo.
O xogo está dispoñible en niptaidea.mougan.es.