
A nova ordem
- Projeto da grade pela IA: o modelo devolve só as coordenadas das casas pretas, sem letras. Pedimos simetria de 180°, de 40% a 60% de casas pretas e que evite listras, escadas longas e ilhas soltas.
- Validação e correção da grade: não confiamos na nota que o modelo dá a si mesmo; quem verifica é o programa. Ele confere se há simetria, se todas as casas brancas estão ligadas, se toda casa branca pertence a uma palavra de duas sílabas ou mais, se a sequência mais longa não passa do limite e se há sequências suficientes de quatro sílabas ou mais. Se algo não bate, ele corrige a grade cortando sequências longas, preenchendo as curtas e abrindo os pontos bloqueados.
- Preenchimento de palavras: preenche primeiro a sequência com menos candidatos compatíveis e, se travar, volta atrás e tenta outra palavra. Se a grade da IA não funcionar, usa uma grade desenhada de antemão (8 por tamanho), e se ainda assim não der, gera uma grade aleatória.
- Escrita das pistas pela IA: só a lista de palavras confirmadas na grade é passada ao modelo. Se ele acrescentar palavras fora da lista ou mudar a grafia, o resultado é descartado, e a resposta também não pode aparecer literalmente na pista.
O modelo é o gemma4 rodando no Ollama do Mac. Como ele é chamado por um endereço compatível com a OpenAI, dá para usar outros modelos só mudando a configuração.
Conhecimentos gerais e atualidades em vez de definições
Pistas que só perguntavam o significado da palavra eram fáceis demais. Por isso acrescentamos pistas no estilo de quiz, como estas.
- A pessoa que disse que a vida, vista de longe, é uma comédia e, vista de perto, uma tragédia (Chaplin)
- Um enorme buraco azul que se forma em áreas de calcário no mar (buraco azul)
- O nome desta famosa ponte de São Francisco (Golden Gate)
- O psicólogo que analisou os sonhos para falar do inconsciente (Freud)
Também acrescentamos pistas de atualidades: o país onde o desabamento de uma geleira causou uma grande enchente, a Copa do Mundo organizada em conjunto por Estados Unidos, México e Canadá, os Jogos Olímpicos de Inverno de Milão-Cortina e assim por diante. Toda vez que uma grade é criada, ele lê as manchetes do Google Notícias e traz mais palavras de artigos aleatórios da Wikipédia.
O banco de palavras tem hoje 1.102 palavras.
- Comprimento: 568 de duas sílabas, 291 de três, 168 de quatro, 74 de cinco
- Área: cotidiano 810, ciência 86, história 60, tecnologia 49, palavras antigas 34, cultura 30, geografia 21, atualidades 12
- Pistas no estilo de quiz: 147
- As palavras são classificadas como comuns, normais ou raras pela posição na frequência em legendas.
As 180 palavras que apareceram mais recentemente em um aparelho ficam guardadas e são excluídas da grade seguinte. Isso por causa do problema de as mesmas palavras aparecerem seguidas.
Dificuldade
- Fácil: 8×8, 18% das casas brancas reveladas no início, respostas de até 5 sílabas
- Médio: 10×10, 8% reveladas, até 6 sílabas
- Difícil: 12×12, nada revelado, até 8 sílabas
Cada dificuldade também tem mínimos diferentes para o número de pistas horizontais e verticais e para os cruzamentos no meio. O difícil exige pelo menos 8 horizontais, 6 verticais e 6 cruzamentos no meio.
Saiu no meio? Continua de onde parou
No começo, o lobby tinha um botão de continuar. Se você escolhia o fácil enquanto estava no meio de uma grade difícil, aparecia o continuar, e ficava confuso se era uma grade nova ou uma que você estava retomando. Por isso mudamos o conceito do lobby.
- No lobby só se criam jogos novos.
- Se houver uma grade em andamento, você vai direto para a sala de jogo em vez de ficar no lobby.
- Na sala de jogo, ao apertar lobby ou jogo novo, ele pergunta se você quer desistir do problema atual.
O problema seguinte era o registro sumir. Se você fazia a última entrada, fechava a janela e depois entrava no lobby, a grade que estava resolvendo era apagada do banco. Ela era apagada durante a limpeza das grades em andamento ao entrar no lobby. Agora, qualquer grade com pelo menos uma casa preenchida fica como "salvamento parcial", e só as grades sem nenhuma entrada são apagadas. A sala de jogo também tem um botão de salvar, e ao reabrir uma grade pela lista de registros você continua resolvendo.
A tela do celular
No celular, o teclado virtual cobria metade da tela. As pistas também eram mostradas com coordenadas como A1, o que as tornava difíceis de achar. Então mudamos.
- Ao tocar no número da grade, aparece primeiro a pista horizontal, e as abas horizontal/vertical alternam entre elas.
- Em vez de uma sílaba por vez, você escreve a resposta inteira e aperta enter, e as pistas horizontais são preenchidas na horizontal e as verticais na vertical.
- Ao tocar em uma casa, o teclado não aparece de imediato, então dá para ver a grade primeiro.
O código real desta etapa
Esta é a API que cria uma grade nova. A ordem grade → palavras → pistas aparece claramente.
app/api/puzzle/route.ts
// 1. 모델이 글자 없는 판 구조를 먼저 제안한다.
const patternCandidates = await generateAiPatternCandidates(difficulty);
// 2. 판의 슬롯 길이가 정해진 뒤 후보 단어를 모아 실제로 채운다.
const dynamicBank = await loadDynamicBank(difficulty, avoidWords, { useLlm: false });
let puzzle: Puzzle | null = null;
let patternSource: "ai" | "curated" = "curated";
// AI의 판 초안을 목표 모양으로 삼고, 연결형 배치기가 실제 단어를 고른다.
for (const rows of patternCandidates) {
puzzle = generatePuzzleFromRows(difficulty, rows, {
avoidWords,
extraBank: dynamicBank.entries,
});
if (puzzle) {
patternSource = "ai";
break;
}
}
if (!puzzle) {
puzzle = generatePuzzle(difficulty, {
avoidWords,
extraBank: dynamicBank.entries,
});
}
if (!puzzleMeets(puzzle, difficulty)) {
puzzle = generatePuzzle(difficulty, { extraBank: dynamicBank.entries });
}
if (!puzzleMeets(puzzle, difficulty)) {
puzzle = generatePuzzle(difficulty);
}
// 3. 최종 선택된 정답만 모델에 전달해 문제를 쓴다.
const selected = puzzle.across
.concat(puzzle.down)
.map(({ word, clue }) => ({ word, clue }));
const generatedClues = await generateCluesForWords(difficulty, selected);
puzzle = applyGeneratedClues(puzzle, generatedClues);Esta é a parte que organiza as grades ao sair para o lobby. As grades vazias são apagadas, e as que têm entradas ficam como salvamento parcial.
lib/db.ts
for (const row of rows) {
if (isEmptyBoard(row)) {
d.prepare(`DELETE FROM moves WHERE code = ?`).run(row.code);
d.prepare(`DELETE FROM games WHERE code = ?`).run(row.code);
continue;
}
d.prepare(
`UPDATE games
SET status = 'abandoned', end_text = COALESCE(NULLIF(end_text, ''), '중간 저장'), updated_at = ?
WHERE code = ?`,
).run(now, row.code);
}O próximo post é "3. O processo de criação e seus problemas, revistos com estatísticas".
COMMENTS · CHAPTER 02
Grade primeiro, palavras depois, pistas por último 0 comentários
Ainda não há comentários. Deixe uma pergunta ou algo que queira acrescentar.
Para publicar um comentário, comece com o Google e registre um apelido e um e-mail.
Começar com o Google