El nuevo orden

  1. Diseño de la cuadrícula por la IA: el modelo solo devuelve las coordenadas de las casillas negras, sin letras. Le pedimos simetría de 180°, entre un 40~60% de casillas negras, y que evite rayas, escaleras largas e islas separadas.
  2. Validación y corrección de la cuadrícula: no nos fiamos de la puntuación que se pone el propio modelo; lo comprueba el programa. Mira si es simétrica, si todas las casillas blancas están conectadas, si cada casilla blanca pertenece a una palabra de dos o más sílabas, si el hueco más largo no supera el límite y si hay suficientes huecos de cuatro o más sílabas. Si algo falla, corrige la cuadrícula cortando las filas largas, rellenando las cortas y abriendo los puntos bloqueados.
  3. Relleno de palabras: se rellena primero el hueco con menos candidatos que encajen y, si se atasca, retrocede y prueba otra palabra. Si la cuadrícula de la IA no sirve, usa una cuadrícula dibujada de antemano (8 por tamaño) y, si tampoco funciona, genera una cuadrícula aleatoria.
  4. Redacción de pistas por la IA: al modelo solo se le pasa la lista de palabras confirmadas en la cuadrícula. Si añade palabras que no están en la lista o cambia la ortografía, se descarta, y la respuesta tampoco puede aparecer tal cual en la pista.

El modelo es gemma4, que se ejecuta en Ollama en el Mac. Se llama a través de una dirección compatible con OpenAI, así que basta con cambiar la configuración para usar otros modelos.

Cultura general y actualidad en lugar de definiciones

Las pistas que solo preguntaban el significado de una palabra eran demasiado fáciles. Así que añadimos pistas de tipo concurso como estas.

  • La persona que dijo que la vida vista de lejos es una comedia y de cerca, una tragedia (Chaplin)
  • Un enorme agujero azul que se forma en zonas de caliza marina (agujero azul)
  • El nombre de este famoso puente de San Francisco (Golden Gate)
  • El psicólogo que analizó los sueños para hablar del inconsciente (Freud)

También añadimos pistas de actualidad: el país donde el derrumbe de un glaciar provocó una gran inundación, el Mundial organizado conjuntamente por Estados Unidos, México y Canadá, los Juegos Olímpicos de Invierno de Milán-Cortina, y otras por el estilo. Cada vez que se crea una cuadrícula, lee los titulares de Google Noticias y saca más palabras de artículos aleatorios de Wikipedia.

El banco de palabras tiene ahora 1,102 palabras.

  • Longitud: 568 de dos sílabas, 291 de tres, 168 de cuatro y 74 de cinco
  • Ámbito: vida cotidiana 810, ciencia 86, historia 60, tecnología 49, palabras antiguas 34, cultura 30, geografía 21, actualidad 12
  • Pistas de tipo concurso: 147
  • Las palabras se clasifican como comunes, normales o raras según su posición en la frecuencia de aparición en subtítulos.

Las 180 palabras que han salido más recientemente en un dispositivo se recuerdan y se excluyen de la siguiente cuadrícula. Es por el problema de que salían las mismas palabras una y otra vez.

Dificultad

  • Fácil: 8×8, se revela el 18% de las casillas blancas al empezar, respuestas de hasta 5 sílabas
  • Media: 10×10, se revela el 8%, hasta 6 sílabas
  • Difícil: 12×12, no se revela nada, hasta 8 sílabas

Cada dificultad tiene también mínimos distintos para el número de pistas horizontales y verticales y para los cruces intermedios. La difícil exige al menos 8 horizontales, 6 verticales y 6 cruces intermedios.

Seguir aunque salgas a mitad

Al principio, la sala de espera tenía un botón de continuar. Si elegías fácil mientras ibas por la mitad de una cuadrícula difícil, aparecía el botón de continuar, y no quedaba claro si era una cuadrícula nueva o la que estabas resolviendo. Así que cambiamos el concepto de la sala de espera.

  • En la sala de espera solo se crean partidas nuevas.
  • Si hay una cuadrícula en curso, no te quedas en la sala de espera y entras directamente en la sala de juego.
  • Si en la sala de juego pulsas sala de espera o partida nueva, te pregunta si quieres abandonar el crucigrama actual.

El siguiente problema era que los registros desaparecían. Si hacías la última entrada, cerrabas la ventana y luego entrabas en la sala de espera, la cuadrícula que estabas resolviendo se borraba de la base de datos. Se borraba al hacer limpieza de las cuadrículas en curso al entrar en la sala de espera. Ahora, cualquier cuadrícula en la que se haya rellenado aunque sea una casilla se guarda como "guardado a medias", y solo se borran las cuadrículas sin ninguna entrada. La sala de juego también tiene un botón de guardar, y al volver a abrir una cuadrícula desde la lista de registros se puede seguir resolviendo.

La pantalla del móvil

En el móvil, el teclado virtual tapaba la mitad de la pantalla. Además, las pistas se mostraban con coordenadas como A1 y costaba encontrarlas. Así que lo cambiamos.

  • Al pulsar un número de la cuadrícula aparece primero la pista horizontal, y con las pestañas horizontal/vertical se cambia de una a otra.
  • En lugar de escribir sílaba a sílaba, escribes la respuesta entera y pulsas Intro, y las pistas horizontales se rellenan en horizontal y las verticales en vertical.
  • Al pulsar una casilla ya no aparece el teclado de inmediato, así que puedes ver primero la cuadrícula.

El código real de esta etapa

Esta es la API que crea una cuadrícula nueva. Se ve tal cual el orden cuadrícula → palabras → pistas.

app/api/puzzle/route.tstypescript · 36 líneas
// 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 es la parte que hace limpieza de las cuadrículas al salir a la sala de espera. Las cuadrículas vacías se borran y las que tienen entradas se conservan como guardado a medias.

lib/db.tstypescript · 12 líneas
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);
}

La siguiente entrada es "3. El proceso de creación y sus problemas, revisados con estadísticas".