Tre modi di generare un cruciverba, e la domanda giusta da farsi per prima

Per un gioco del genere ci sono in pratica tre strade. Schema fisso: si preparano N griglie con parole e indizi già incrociati a mano, e il gioco ne pesca una a caso — qualità garantita, ma contenuto finito che si ripete dopo poche partite. Generazione dinamica: si tiene solo un dizionario di parole con indizio, e un algoritmo di riempimento (backtracking su una griglia con caselle nere) incrocia parole a caso a ogni partita — varietà infinita, ma è un problema NP-hard non banale, con tempi di generazione lato client che possono esplodere. Semi-dinamico: schemi di griglia fissi nella forma, ma le parole che ci entrano vengono pescate a caso da un pool compatibile a ogni partita — un compromesso ragionevole tra varietà e semplicità, ed è la strada da cui sono partito.

Il primo motore: schemi fissi e riempimento a backtracking

Ho costruito il gioco completo — griglia, indizi, timer, tastiera, salvataggio dei record in locale — con un dizionario iniziale di circa 200 parole con indizio (3-8 lettere) e tre schemi di griglia fissi nella forma (celle bianche e nere), riempiti a ogni partita da un algoritmo di backtracking: prova una parola compatibile per lunghezza in ogni spazio libero, e se un incrocio non torna, torna indietro e ne prova un'altra. Uno stress test automatico in Node — 200 iterazioni per ciascuno dei tre schemi — ha dato un risultato netto: fallimento praticamente sempre, anche dopo aver ampliato il dizionario a quasi 400 voci.

Il vero problema non era il dizionario, era la rigidità della griglia

La causa non era la quantità di parole disponibili, ma la struttura degli schemi stessi. Uno dei tre aveva una parola verticale che attraversava tutte le righe della griglia, incrociando sei parole orizzontali insieme — e in ognuno di quei punti serve che l'ultima lettera di una parola orizzontale coincida esattamente con la lettera richiesta dalla verticale in quella posizione. In italiano le parole finiscono quasi sempre in vocale e iniziano spesso in consonante: una combinazione di vincoli simultanei strutturalmente troppo rigida, che nessuna quantità ragionevole di parole aggiuntive avrebbe risolto — servirebbero migliaia di voci, non centinaia, per coprire ogni combinazione possibile in quel punto della griglia.

La lezione che ha cambiato l'approccio. Un dizionario più grande non risolve un vincolo di incrocio strutturalmente troppo denso: a un certo punto il problema non è più "quante parole ho", ma "quante combinazioni simultanee lettera-per-lettera sto chiedendo di soddisfare in un solo punto della griglia".

Generare la griglia a runtime non è bastato da solo

Il tentativo successivo è stato smettere di usare tre schemi fissi e generare la forma della griglia proceduralmente a ogni partita, ritentando con una disposizione diversa in caso di fallimento — stesso algoritmo di backtracking già validato, ma con la possibilità di cambiare schema invece di restare bloccati sempre sullo stesso. Anche qui, però, gli stress test hanno mostrato lo stesso limite: alcune combinazioni di incroci restano matematicamente troppo vincolate per un dizionario scritto a mano, indipendentemente da quanti schemi diversi si generano.

La soluzione: un motore a incastro libero

A quel punto ho cambiato completamente impostazione: invece di risolvere una griglia vuota già disegnata (il classico problema di soddisfacimento di vincoli, o CSP, con cui si costruiscono la maggior parte dei generatori di cruciverba), il motore costruisce la griglia mentre gioca. Parte da una parola pescata a caso dal dizionario, poi ne aggiunge altre una alla volta, agganciando ciascuna nuova parola a una lettera già presente in una parola già piazzata, e verificando che non si creino conflitti con le celle vicine. La forma finale della griglia emerge dalle parole stesse: non c'è mai uno schema pre-esistente che deve per forza tornare, quindi non esiste uno stato "impossibile" in cui il riempimento può bloccarsi — si costruisce solo ciò che effettivamente si incastra.

Validato offline in Python, dove il tempo di calcolo non è un vincolo: 100 tentativi su 100 riusciti, su entrambi i livelli di difficoltà, con griglie compatte e ben interconnesse. Portato poi in JavaScript per girare nel browser.

Un bug che sembrava strutturale, ed era solo un ordine di esecuzione

Durante il porting in JavaScript, uno stress test estratto a parte per verificare l'esatto codice del browser continuava a fallire per un motivo apparentemente serio: l'elenco di tutte le parole disponibili risultava vuoto, anche se il dizionario risultava caricato correttamente. Non era un difetto dell'algoritmo, ma un problema nel mio script di estrazione per il test: l'ordine con cui i due blocchi di script venivano letti fuori dal contesto reale della pagina non rispettava l'ordine di esecuzione effettivo nel browser. Corretta l'estrazione, lo stress test finale ha dato 200 successi su 200 su entrambe le difficoltà, in circa 26 millisecondi a generazione.

Due bug di interfaccia nati proprio dalle intersezioni

Il primo: scrivere in orizzontale su una cella condivisa tra due parole faceva scattare inaspettatamente la direzione verticale, come se quella cella fosse stata cliccata due volte di proposito. La causa era che la funzione di spostamento del focus impostava la cella come "attiva" prima ancora di darle davvero il focus — così, quando partiva la gestione del focus, il codice vedeva "stessa cella già attiva" anche durante la digitazione automatica in sequenza, non solo durante un doppio click volontario. La correzione è stata separare i due casi: il focus, anche quello automatico durante la digitazione, non tocca più la direzione; il cambio di direzione scatta solo se c'è un vero click su una cella che era già quella attiva prima del click, tracciato a parte con eventi di pressione diretti sul puntatore.

Il secondo: su una lettera già scritta da una parola incrociata, digitare di nuovo la stessa lettera bloccava l'avanzamento automatico. La casella di testo aveva un limite di un carattere, e se si digita lo stesso carattere già presente senza che il contenuto sia selezionato in anticipo, il browser non lo conta come una modifica — quindi nessun evento, nessun avanzamento. La correzione è stata doppia: selezionare il contenuto della cella a ogni focus, così digitare lo sostituisce sempre, più una gestione diretta del tasto premuto come rete di sicurezza per alcune tastiere mobili che non emettono l'evento standard.

Il gioco non è solo l'algoritmo: sfondo, dizionario, PWA

Con il motore stabile, sono rimasti i dettagli che fanno la differenza tra "funziona" e "sta bene nel sito". L'animazione di sfondo condivisa con gli altri giochi (linee animate su canvas) mancava, ed è stata aggiunta con lo stesso identico pattern usato altrove — nessuna reinvenzione. Il dizionario è cresciuto da 399 a 671 parole, concentrando l'ampliamento sulle lunghezze più povere invece che su quelle già coperte, e ogni aggiunta è stata validata contro duplicati prima di essere unita al pool esistente. Con il dizionario più grande, un nuovo stress test ha dato 300 successi su 300 su entrambe le difficoltà, circa 35 millisecondi a generazione.

Ultimo passaggio, il lato PWA: icone generate con la stessa convenzione squircle usata per tutte le altre app del sito, tenendo l'artwork originale intero — non quadrato in partenza — invece di ritagliarlo, mettendolo su una tela quadrata con un piccolo bordo del colore del sito. Su iPhone, tre correzioni mirate: rimosso l'alone grigio al tocco su celle e pulsanti, attivata una risposta immediata al tap invece del ritardo di default del browser, e disattivata l'autocorrezione sulle caselle di testo — altrimenti iOS prova a "correggere" le lettere del cruciverba mentre si gioca.

Pubblicare il gioco: redirect, sitemap, e un audit SEO che ha trovato testo bugiardo

Integrare il gioco nella pagina dei giochi del sito ha richiesto anche un piccolo intervento di layout: ridurre leggermente le dimensioni delle card per farne stare tre per riga invece di due, mantenendo invariato il comportamento mobile. Poi il lavoro meno visibile ma altrettanto necessario: il redirect da URL senza estensione a quello con .html in configurazione, e la voce corrispondente nella sitemap — senza questi due passaggi, il motore di ricerca finisce per vedere due varianti della stessa pagina come pagine diverse.

L'audit SEO finale ha trovato un problema più interessante di un semplice limite di caratteri superato nella meta description: in tre punti della pagina — il paragrafo introduttivo, la sezione FAQ visibile, e i dati strutturati JSON-LD della stessa FAQ — il testo diceva ancora "quasi 200 parole" e descriveva il motore come "schemi fissi validati con backtracking". Frasi vere quando erano state scritte, diventate false nel momento esatto in cui l'algoritmo è cambiato — e sopravvissute in silenzio, perché nessuno le aveva più ricontrollate dopo quel cambio. Corrette tutte e tre, in modo che il testo raccontasse esattamente cosa il gioco fa oggi.

Cosa mi porto a casa

La lezione più utile è che un dizionario più grande non risolve un vincolo di incrocio strutturalmente troppo denso: superata una certa soglia il problema smette di essere "quante parole ho" e diventa "quante combinazioni simultanee sto chiedendo di soddisfare in un solo punto della griglia" — a quel punto la soluzione robusta non è più dati, è un algoritmo diverso. Costruire la griglia parola per parola invece di risolverne una già disegnata elimina alla radice ogni stato "impossibile": non è un compromesso sulla qualità, è semplicemente un problema diverso e più facile. E infine, i bug più fastidiosi in questo lavoro non sono mai stati le decisioni sull'algoritmo, ma i dettagli piccoli e silenziosi delle interazioni reali — un focus impostato un attimo troppo presto, un carattere identico che il browser non registra come cambiamento, un testo descrittivo che nessuno aggiorna dopo aver cambiato la logica sottostante.

Se ti interessa un altro gioco costruito da zero in JavaScript vanilla per il sito, ho scritto anche a proposito delle regole e dell'IA a tre livelli del Burraco online. E se ti va di vedere il risultato finale, il cruciverba è giocabile gratis nella sezione giochi del sito.

Domande frequenti

Perché un algoritmo di backtracking su una griglia fissa può fallire nel generare un cruciverba anche con centinaia di parole nel dizionario?

Perché il vincolo che conta non è quante parole ci sono in totale, ma quante intersezioni simultanee servono in un punto della griglia. Una parola verticale che attraversa sei parole orizzontali insieme, in punti dove serve che l'ultima lettera di una orizzontale coincida con la prima di una verticale, richiede una copertura di combinazioni lettera-per-lettera che solo un dizionario di migliaia di voci può garantire in modo affidabile — poche centinaia non bastano, indipendentemente da quante se ne aggiungono.

Cos'è un generatore di cruciverba "a incastro libero" e come funziona?

Invece di riempire uno schema vuoto già disegnato, il generatore costruisce la griglia: parte da una parola, poi ne aggiunge altre una alla volta agganciandole a una lettera già presente in una parola piazzata in precedenza, verificando che non ci siano conflitti. La forma finale della griglia emerge dalle parole stesse, quindi il puzzle è risolvibile per costruzione — non esiste uno stato "impossibile" in cui il riempimento può bloccarsi.

Perché conviene generare la griglia di un cruciverba a runtime invece di usare pochi schemi fissi pre-disegnati?

Perché ogni schema fisso porta con sé un pattern di incroci rigido — ad esempio una parola che ne attraversa sei altre insieme — che il dizionario o soddisfa sempre o non soddisfa mai: con pochi schemi il puzzle finisce per fallire sempre nello stesso punto. Generare (o pre-risolvere) più combinazioni permette di ritentare con un'impostazione diversa invece di sbattere sempre contro lo stesso vincolo.

Perché una cella di intersezione tra due parole in un cruciverba web può scambiare la digitazione normale per un doppio click?

Perché se il codice imposta la cella come "attiva" prima ancora di spostarci il focus, quando scatta l'evento di focus il sistema vede "stessa cella già attiva" anche durante la digitazione sequenziale normale, non solo durante un doppio click volontario. La correzione è separare la navigazione (il focus, anche automatico) dal vero click deliberato, tracciato a parte con mousedown o touchstart.

Perché in una casella di testo con maxlength=1 scrivere di nuovo la stessa lettera già presente a volte non fa avanzare l'input?

Perché il browser non genera un evento di cambiamento se il carattere digitato è identico a quello già presente e il contenuto del campo non è selezionato in anticipo. La correzione affidabile è selezionare il contenuto della cella a ogni focus, così la digitazione lo sostituisce sempre, più una gestione diretta del tasto premuto come rete di sicurezza su alcune tastiere mobili.

Perché conviene rifare un audit SEO e dei contenuti dopo un cambio radicale dell'algoritmo o della logica interna di una pagina?

Perché un testo scritto per descrivere "come funziona" una pagina — numeri, nomi di tecniche, conteggi — diventa falso nel momento esatto in cui l'implementazione cambia, e sopravvive in silenzio sia nel testo visibile sia nei dati strutturati JSON-LD, che i motori di ricerca e gli assistenti AI leggono entrambi come fonte di verità. Vale la pena ricontrollarlo esplicitamente dopo ogni cambio sostanziale, non solo alla prima pubblicazione.