
Puzles de palabras
Formas, adivinas o completas palabras a partir de letras sueltas o de pistas, como en anagramas, crucigramas o sopas de letras. El reto es tu vocabulario y tu ortografía.
También se llama: juegos de palabras, crucigrama, sopa de letras, ahorcado
Mecánicas que lo definen
- Letras como piezas, que se eligen, se colocan en una cuadrícula o se enlazan entre vecinas para formar palabras.
- Pistas sobre una palabra oculta (letras acertadas, posiciones, definiciones) que reducen poco a poco las posibilidades.
- Un diccionario que decide qué cuenta como palabra válida y, a menudo, puntos según la longitud o las letras raras.
- Límites que dan tensión, como un número de intentos, un reloj o fichas que amenazan con acabar la partida.
Bucle de juego
Mirar las letras y las pistas, pensar una palabra que encaje, probarla y usar la respuesta del juego para descartar opciones en el siguiente intento.
Demo propia
Controles. Escribe con el teclado y pulsa Enter para comprobar; Retroceso borra. También puedes tocar o hacer clic en el teclado de la pantalla, que incluye Enter y ⌫.
Abrir en pantalla completaTienes seis intentos para adivinar una palabra de cinco letras. Después de cada intento, cada letra se pinta de verde si está en su sitio, de amarillo si está en la palabra pero en otro sitio y de gris si no está. Con esas pistas vas descartando palabras hasta dar con la correcta.
Hecha con Kaplay, en unas 151 líneas de JavaScript.
Cómo está hecha
Configuración
Se crea el juego con tamaño fijo. Las constantes definen el largo de la palabra, el número de intentos, el tamaño de las casillas y los tres colores de pista. RANK ordena las pistas para que una tecla del teclado conserve siempre la mejor que se conoce.
// Played with the physical keyboard or by tapping the on-screen keyboard, // so it needs no extra touch buttons. kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [18, 18, 26], buttons: { start: { keyboard: ["space", "enter"] } }, }); const WORD_LENGTH = 5; const MAX_TRIES = 6; const TILE = 40; // size of a letter square const GAP = 6; const COLORS = { empty: rgb(32, 32, 42), correct: rgb(83, 141, 78), // right letter, right place present: rgb(186, 156, 52), // in the word, somewhere else absent: rgb(52, 52, 62), // not in the word }; const RANK = { absent: 1, present: 2, correct: 3 }; // a key keeps its best hintLas listas de palabras
Hay una lista de palabras cotidianas por idioma y una distribución de teclado para cada uno (la española lleva Ñ). El idioma de la demo decide cuál se usa.
// Small lists of everyday words. The hidden word is drawn from them; any // five letters are accepted as a guess (see the exercises). const ANSWERS = { es: ["PLATO", "LIBRO", "NUBES", "PERRO", "CAMPO", "RELOJ", "SILLA", "LLAVE", "FUEGO", "PIANO", "VERDE", "BARCO", "TIGRE", "CARTA", "PAPEL", "RUEDA", "NIEVE", "HUEVO", "CIELO", "MUNDO", "NOCHE", "PLUMA", "SUEÑO", "PUNTO"], en: ["PLANT", "CLOUD", "BREAD", "RIVER", "HOUSE", "LIGHT", "STONE", "TRAIN", "CHAIR", "MUSIC", "OCEAN", "GRAPE", "APPLE", "HORSE", "BEACH", "SMILE", "WATER", "SPOON", "CLOCK", "BRICK", "TIGER", "PAPER", "NIGHT", "GREEN"], }; const KEY_ROWS = { es: ["QWERTYUIOP", "ASDFGHJKLÑ", "ZXCVBNM"], en: ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"], }; const LETTERS = KEY_ROWS[lang].join("");Puntuar un intento
Es la regla que define el juego, y necesita dos pasadas para ser justa con las letras repetidas. En la primera se marcan en verde las letras que coinciden en su posición y se cuentan las letras de la respuesta que sobran. En la segunda, una letra del intento es amarilla solo si todavía queda una copia sin usar en la respuesta. Así, si la respuesta tiene una sola E y escribes dos, solo una se pinta.
// The rule that defines the genre. Two passes, so repeated letters are fair: // 1) exact matches are "correct" and use up that letter of the answer; // 2) the remaining guess letters are "present" only while unused copies remain. function score(guess, answer) { const result = Array(WORD_LENGTH).fill("absent"); const unused = {}; // letters of the answer not matched exactly, with counts for (let i = 0; i < WORD_LENGTH; i++) { if (guess[i] === answer[i]) result[i] = "correct"; else unused[answer[i]] = (unused[answer[i]] ?? 0) + 1; } for (let i = 0; i < WORD_LENGTH; i++) { if (result[i] !== "correct" && unused[guess[i]] > 0) { result[i] = "present"; unused[guess[i]]--; } } return result; }El tablero
Una cuadrícula de 6 por 5 casillas. Cada casilla es un rectángulo con un texto hijo para la letra, y una matriz tiles[fila][columna] permite cambiar cualquiera de ellas.
// A 6×5 grid of squares; each one is a rectangle with a letter on top. add([text(t({ es: "Adivina la palabra", en: "Guess the word" }), { size: 20, font: FONT }), pos(40, 12)]); const tiles = []; for (let r = 0; r < MAX_TRIES; r++) { tiles.push([]); for (let c = 0; c < WORD_LENGTH; c++) { const box = add([rect(TILE, TILE, { radius: 4 }), pos(40 + c * (TILE + GAP) + TILE / 2, 66 + r * (TILE + GAP)), anchor("center"), color(COLORS.empty), outline(2, rgb(70, 70, 90)), scale(1)]); box.add([text("", { size: 22, font: FONT }), anchor("center")]); tiles[r].push(box); } } const setLetter = (c, ch) => (tiles[row][c].children[0].text = ch);El teclado en pantalla
Cada tecla es un objeto con area() y onClick(), así funciona igual con ratón que con el dedo. Su color muestra la mejor pista conocida de esa letra, y esa memoria visual es la que ayuda a planear el siguiente intento.
// The on-screen keyboard: every key is a clickable object. Its color shows // the best hint known for that letter, which helps plan the next guess. const keys = {}; const keyState = {}; function addKey(label, x, y, w, action) { const key = add([rect(w, 40, { radius: 4 }), pos(x, y), color(rgb(128, 128, 145)), area(), "key"]); key.add([text(label, { size: label.length > 1 ? 13 : 17, font: FONT }), pos(w / 2, 20), anchor("center")]); key.onClick(action); return key; } KEY_ROWS[lang].forEach((letters, r) => { const extra = r === 2 ? 2 * (48 + 3) : 0; // the last row also has Enter and Delete let x = 466 - (letters.length * 32 - 3 + extra) / 2; const y = 120 + r * 46; if (r === 2) (addKey("Enter", x, y, 48, submit), (x += 51)); for (const ch of letters) (keys[ch] = addKey(ch, x, y, 29, () => typeLetter(ch))), (x += 32); if (r === 2) addKey("⌫", x, y, 48, erase); }); function paintKey(ch, hint) { if ((RANK[hint] ?? 0) <= (RANK[keyState[ch]] ?? 0)) return; keyState[ch] = hint; keys[ch].color = COLORS[hint]; }Escribir
Las letras llenan la fila actual de izquierda a derecha. onCharInput recibe las del teclado físico, quita las tildes y conserva la Ñ. Las teclas de la pantalla llaman a las mismas funciones, así que no importa de dónde venga la letra.
// Letters fill the current row from left to right; Delete removes the last // one and Enter checks the row. Accents are dropped (á → A), ñ is kept. const message = add([text("", { size: 15, font: FONT, align: "center", width: 330 }), pos(466, 300), anchor("center"), color(255, 220, 140)]); const say = (texts) => (message.text = t(texts)); function typeLetter(ch) { if (busy || guess.length >= WORD_LENGTH) return; setLetter(guess.length, ch); guess += ch; say({ es: "", en: "" }); } function erase() { if (busy || !guess.length) return; guess = guess.slice(0, -1); setLetter(guess.length, ""); } onCharInput((ch) => { const up = ch === "ñ" ? "Ñ" : ch.normalize("NFD").replace(/[̀-ͯ]/g, "").toUpperCase(); if (up.length === 1 && LETTERS.includes(up)) typeLetter(up); }); onKeyPressRepeat("backspace", erase); onKeyPress("enter", submit);Revelar las pistas
Al pulsar Enter se puntúa la fila y las casillas se pintan una a una con un pequeño salto (wait y tween). Mientras dura, la variable busy bloquea la entrada. La pausa crea suspenso y da tiempo a leer cada pista. Luego se decide si ganaste, si perdiste o si pasas a la fila siguiente.
// Checking a row colors its squares one by one, with a small "pop". The // short pause builds suspense and lets you read each hint. function submit() { if (busy) return; if (guess.length < WORD_LENGTH) return say({ es: "Faltan letras", en: "Not enough letters" }); busy = true; const hints = score(guess, answer); const [r, word] = [row, guess]; hints.forEach((hint, c) => wait(c * 0.18, () => { tiles[r][c].color = COLORS[hint]; tiles[r][c].scale = vec2(1.15); tween(1.15, 1, 0.15, (s) => (tiles[r][c].scale = vec2(s))); paintKey(word[c], hint); }), ); wait(WORD_LENGTH * 0.18 + 0.2, () => { if (word === answer) return wait(0.8, () => go("end", true, answer, r + 1)); if (r + 1 >= MAX_TRIES) return wait(0.8, () => go("end", false, answer, MAX_TRIES)); row++; guess = ""; busy = false; say({ es: `Intento ${row + 1} de ${MAX_TRIES}`, en: `Attempt ${row + 1} of ${MAX_TRIES}` }); }); } say({ es: `Intento 1 de ${MAX_TRIES}`, en: `Attempt 1 of ${MAX_TRIES}` });Escenas
Hay tres escenas (title, game y end). La escena final dice cuántos intentos usaste y, si no acertaste, revela la palabra.
function screen(title, subtitle) { add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 46)), anchor("center")]); add([text(subtitle, { size: 16, font: FONT, align: "center", width: 540, lineSpacing: 4 }), pos(center().add(0, 34)), anchor("center"), color(200, 205, 230)]); onButtonPress("start", () => go("game")); onClick(() => go("game")); } scene("title", () => screen( t({ es: "Puzles de palabras", en: "Word puzzle" }), t({ es: "Adivina la palabra de 5 letras en 6 intentos. Verde: letra en su sitio. Amarillo: está en otro sitio. Gris: no está.\nEscribe con el teclado o toca las teclas. Pulsa espacio o toca para empezar.", en: "Guess the 5-letter word in 6 tries. Green: right letter, right place. Yellow: it's somewhere else. Grey: not in the word.\nType or tap the keys. Press space or tap to start.", }), ), ); scene("end", (won, answer, tries) => screen( won ? t({ es: "¡Acertaste!", en: "You got it!" }) : t({ es: "Se acabaron los intentos", en: "Out of tries" }), (won ? t({ es: `En ${tries} de ${MAX_TRIES} intentos. `, en: `In ${tries} of ${MAX_TRIES} tries. ` }) : "") + t({ es: `La palabra era ${answer}.\nPulsa espacio para jugar otra vez.`, en: `The word was ${answer}.\nPress space to play again.` }), ), ); go("title");
Qué simplificamos
- Se acepta cualquier combinación de cinco letras como intento. Un juego real solo admite palabras que existen, y para eso necesita un diccionario de miles de palabras.
- La palabra sale al azar de una lista de 24 por idioma, no hay una palabra del día común a todos, ni estadísticas, ni modo difícil.
- Las tildes se ignoran (á cuenta como A) para que la lista y el teclado sean más sencillos; la Ñ sí es una letra propia.
Para modificarla
- Agrega una lista de palabras válidas y rechaza los intentos que no estén en ella, con un mensaje y una pequeña sacudida de la fila.
- Haz que la palabra dependa de la fecha, para que todos los que juegan el mismo día tengan la misma.
- Implementa el modo difícil, en el que cada intento debe usar las pistas verdes y amarillas que ya conoces.
- Cambia WORD_LENGTH a 6 y prepara una lista de palabras de seis letras. ¿Qué más hay que tocar en el código?
Código completo
// Word puzzle demo
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { lang, t, FONT, pixelDensity } from "../common.js";
// #region setup
// Played with the physical keyboard or by tapping the on-screen keyboard,
// so it needs no extra touch buttons.
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [18, 18, 26],
buttons: { start: { keyboard: ["space", "enter"] } },
});
const WORD_LENGTH = 5;
const MAX_TRIES = 6;
const TILE = 40; // size of a letter square
const GAP = 6;
const COLORS = {
empty: rgb(32, 32, 42),
correct: rgb(83, 141, 78), // right letter, right place
present: rgb(186, 156, 52), // in the word, somewhere else
absent: rgb(52, 52, 62), // not in the word
};
const RANK = { absent: 1, present: 2, correct: 3 }; // a key keeps its best hint
// #endregion
// #region words
// Small lists of everyday words. The hidden word is drawn from them; any
// five letters are accepted as a guess (see the exercises).
const ANSWERS = {
es: ["PLATO", "LIBRO", "NUBES", "PERRO", "CAMPO", "RELOJ", "SILLA", "LLAVE", "FUEGO", "PIANO", "VERDE", "BARCO",
"TIGRE", "CARTA", "PAPEL", "RUEDA", "NIEVE", "HUEVO", "CIELO", "MUNDO", "NOCHE", "PLUMA", "SUEÑO", "PUNTO"],
en: ["PLANT", "CLOUD", "BREAD", "RIVER", "HOUSE", "LIGHT", "STONE", "TRAIN", "CHAIR", "MUSIC", "OCEAN", "GRAPE",
"APPLE", "HORSE", "BEACH", "SMILE", "WATER", "SPOON", "CLOCK", "BRICK", "TIGER", "PAPER", "NIGHT", "GREEN"],
};
const KEY_ROWS = {
es: ["QWERTYUIOP", "ASDFGHJKLÑ", "ZXCVBNM"],
en: ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"],
};
const LETTERS = KEY_ROWS[lang].join("");
// #endregion
// #region score
// The rule that defines the genre. Two passes, so repeated letters are fair:
// 1) exact matches are "correct" and use up that letter of the answer;
// 2) the remaining guess letters are "present" only while unused copies remain.
function score(guess, answer) {
const result = Array(WORD_LENGTH).fill("absent");
const unused = {}; // letters of the answer not matched exactly, with counts
for (let i = 0; i < WORD_LENGTH; i++) {
if (guess[i] === answer[i]) result[i] = "correct";
else unused[answer[i]] = (unused[answer[i]] ?? 0) + 1;
}
for (let i = 0; i < WORD_LENGTH; i++) {
if (result[i] !== "correct" && unused[guess[i]] > 0) {
result[i] = "present";
unused[guess[i]]--;
}
}
return result;
}
// #endregion
scene("game", () => {
const answer = choose(ANSWERS[lang]);
let row = 0; // current attempt
let guess = ""; // letters typed in the current row
let busy = false; // true while a row is being revealed
// #region board
// A 6×5 grid of squares; each one is a rectangle with a letter on top.
add([text(t({ es: "Adivina la palabra", en: "Guess the word" }), { size: 20, font: FONT }), pos(40, 12)]);
const tiles = [];
for (let r = 0; r < MAX_TRIES; r++) {
tiles.push([]);
for (let c = 0; c < WORD_LENGTH; c++) {
const box = add([rect(TILE, TILE, { radius: 4 }), pos(40 + c * (TILE + GAP) + TILE / 2, 66 + r * (TILE + GAP)),
anchor("center"), color(COLORS.empty), outline(2, rgb(70, 70, 90)), scale(1)]);
box.add([text("", { size: 22, font: FONT }), anchor("center")]);
tiles[r].push(box);
}
}
const setLetter = (c, ch) => (tiles[row][c].children[0].text = ch);
// #endregion
// #region keyboard
// The on-screen keyboard: every key is a clickable object. Its color shows
// the best hint known for that letter, which helps plan the next guess.
const keys = {};
const keyState = {};
function addKey(label, x, y, w, action) {
const key = add([rect(w, 40, { radius: 4 }), pos(x, y), color(rgb(128, 128, 145)), area(), "key"]);
key.add([text(label, { size: label.length > 1 ? 13 : 17, font: FONT }), pos(w / 2, 20), anchor("center")]);
key.onClick(action);
return key;
}
KEY_ROWS[lang].forEach((letters, r) => {
const extra = r === 2 ? 2 * (48 + 3) : 0; // the last row also has Enter and Delete
let x = 466 - (letters.length * 32 - 3 + extra) / 2;
const y = 120 + r * 46;
if (r === 2) (addKey("Enter", x, y, 48, submit), (x += 51));
for (const ch of letters) (keys[ch] = addKey(ch, x, y, 29, () => typeLetter(ch))), (x += 32);
if (r === 2) addKey("⌫", x, y, 48, erase);
});
function paintKey(ch, hint) {
if ((RANK[hint] ?? 0) <= (RANK[keyState[ch]] ?? 0)) return;
keyState[ch] = hint;
keys[ch].color = COLORS[hint];
}
// #endregion
// #region typing
// Letters fill the current row from left to right; Delete removes the last
// one and Enter checks the row. Accents are dropped (á → A), ñ is kept.
const message = add([text("", { size: 15, font: FONT, align: "center", width: 330 }), pos(466, 300), anchor("center"), color(255, 220, 140)]);
const say = (texts) => (message.text = t(texts));
function typeLetter(ch) {
if (busy || guess.length >= WORD_LENGTH) return;
setLetter(guess.length, ch);
guess += ch;
say({ es: "", en: "" });
}
function erase() {
if (busy || !guess.length) return;
guess = guess.slice(0, -1);
setLetter(guess.length, "");
}
onCharInput((ch) => {
const up = ch === "ñ" ? "Ñ" : ch.normalize("NFD").replace(/[̀-ͯ]/g, "").toUpperCase();
if (up.length === 1 && LETTERS.includes(up)) typeLetter(up);
});
onKeyPressRepeat("backspace", erase);
onKeyPress("enter", submit);
// #endregion
// #region reveal
// Checking a row colors its squares one by one, with a small "pop". The
// short pause builds suspense and lets you read each hint.
function submit() {
if (busy) return;
if (guess.length < WORD_LENGTH) return say({ es: "Faltan letras", en: "Not enough letters" });
busy = true;
const hints = score(guess, answer);
const [r, word] = [row, guess];
hints.forEach((hint, c) =>
wait(c * 0.18, () => {
tiles[r][c].color = COLORS[hint];
tiles[r][c].scale = vec2(1.15);
tween(1.15, 1, 0.15, (s) => (tiles[r][c].scale = vec2(s)));
paintKey(word[c], hint);
}),
);
wait(WORD_LENGTH * 0.18 + 0.2, () => {
if (word === answer) return wait(0.8, () => go("end", true, answer, r + 1));
if (r + 1 >= MAX_TRIES) return wait(0.8, () => go("end", false, answer, MAX_TRIES));
row++;
guess = "";
busy = false;
say({ es: `Intento ${row + 1} de ${MAX_TRIES}`, en: `Attempt ${row + 1} of ${MAX_TRIES}` });
});
}
say({ es: `Intento 1 de ${MAX_TRIES}`, en: `Attempt 1 of ${MAX_TRIES}` });
// #endregion
});
// #region scenes
function screen(title, subtitle) {
add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 46)), anchor("center")]);
add([text(subtitle, { size: 16, font: FONT, align: "center", width: 540, lineSpacing: 4 }), pos(center().add(0, 34)), anchor("center"), color(200, 205, 230)]);
onButtonPress("start", () => go("game"));
onClick(() => go("game"));
}
scene("title", () =>
screen(
t({ es: "Puzles de palabras", en: "Word puzzle" }),
t({
es: "Adivina la palabra de 5 letras en 6 intentos. Verde: letra en su sitio. Amarillo: está en otro sitio. Gris: no está.\nEscribe con el teclado o toca las teclas. Pulsa espacio o toca para empezar.",
en: "Guess the 5-letter word in 6 tries. Green: right letter, right place. Yellow: it's somewhere else. Grey: not in the word.\nType or tap the keys. Press space or tap to start.",
}),
),
);
scene("end", (won, answer, tries) =>
screen(
won ? t({ es: "¡Acertaste!", en: "You got it!" }) : t({ es: "Se acabaron los intentos", en: "Out of tries" }),
(won ? t({ es: `En ${tries} de ${MAX_TRIES} intentos. `, en: `In ${tries} of ${MAX_TRIES} tries. ` }) : "") +
t({ es: `La palabra era ${answer}.\nPulsa espacio para jugar otra vez.`, en: `The word was ${answer}.\nPress space to play again.` }),
),
);
go("title");
// #endregionRetos de diseño
- Elegir bien las listas de palabras. Las respuestas deben ser palabras conocidas, pero el diccionario de intentos válidos tiene que ser amplio para no castigar al jugador.
- Cada idioma tiene sus letras (ñ, tildes, dígrafos) y su frecuencia de letras, así que un juego no se traduce, se adapta.
- Que las pistas sean exactas y justas, también con letras repetidas.
- Equilibrar la dificultad para que la partida no dependa de la suerte del primer intento.
Historia
- 1978OrigenHangman
Hangman, de Atari para la VCS, lleva a una consola doméstica el ahorcado de papel y lápiz.
Hangman (video game) (Wikipedia) - 1991
Wordtris, publicado por Spectrum HoloByte, mezcla las palabras con las piezas que caen al estilo de Tetris. Formas palabras con las letras que van bajando.
Wordtris (Wikipedia) - 2003Bookworm
Bookworm, de PopCap, propone una cuadrícula de letras que se enlazan para formar palabras, con fichas en llamas que amenazan con terminar la partida.
Bookworm (Wikipedia) - 2009Words with Friends
Words with Friends lleva las palabras cruzadas al estilo de Scrabble al móvil, con turnos asíncronos contra amigos en línea.
Words with Friends (Wikipedia) - 2012
Letterpress, de Loren Brichter, convierte una cuadrícula de 5 por 5 letras en un territorio que dos jugadores se disputan formando palabras.
Letterpress (Wikipedia) - 2021Wordle
Wordle, un reto diario de adivinar una palabra de cinco letras, se vuelve viral en la web.
Wordle (Wikipedia) - 2022
The New York Times compra Wordle, y el juego se adapta enseguida a decenas de idiomas, como Termo en portugués.
Wordle (Wikipedia) - 2022Absurdle
Absurdle, de qntm, le da la vuelta a Wordle. La respuesta cambia en cada turno para esquivarte, y el puzle pasa a ser acorralar al juego.
Absurdle (Wikipedia)
Juegos de referencia
1978
Ahorcado(Hangman)
El juego del ahorcado en una consola doméstica. Adivinas las letras de una palabra oculta y cada fallo añade una pieza a la horca. Traía 510 palabras de hasta seis letras y cuatro niveles de dificultad.
2003
Bookworm
Unes letras vecinas de una cuadrícula para formar palabras; las que usas desaparecen y caen otras desde arriba. Unas fichas en llamas bajan quemando las letras de debajo, y si una llega al fondo pierdes.
2009
Words with Friends
Un juego de formar palabras cruzadas sobre un tablero, al estilo de Scrabble, contra amigos o desconocidos en línea. Los turnos son asíncronos, así que puedes tener muchas partidas abiertas a la vez y el móvil te avisa cuando te toca.
2021
Wordle
Tienes seis intentos para adivinar una palabra de cinco letras, y después de cada uno las letras se pintan de verde, amarillo o gris según estén en su sitio, en otro sitio o no estén. Hay una sola palabra al día para todos, lo que la volvió un tema de conversación y un fenómeno viral.
- Jugar en el navegadorThe New York TimesTiene publicidad
- Más informaciónWikipedia
2022
Absurdle
Se juega como Wordle, pero el juego hace trampa a propósito. En cada turno cambia la respuesta por otra que encaje con tus pistas, eligiendo la que menos palabras descarte. Tienes intentos ilimitados y el reto es acorralarlo hasta que solo quede una palabra posible.
Ejercicio
Juega la demo y explica por escrito por qué la puntuación necesita dos pasadas. Prueba un intento con dos letras iguales cuando la respuesta solo tiene una. Después agrega una lista de palabras válidas y compara qué cambia en la dificultad.