Cómo pensar un juego
En resumen: Un juego es estado, reglas, entrada, un bucle y un final; el azar con semilla lo hace comprobable, y la lógica vive en funciones que devuelven un valor y se pueden probar.
Problema
¿Por dónde empieza un juego?
Quieres escribir un juego. Por ejemplo, una carrera de dados: una ficha sale de la casilla 0, tiras un dado y avanza hasta llegar a la casilla 20.
Abres el editor y... ¿qué escribes primero? ¿El menú? ¿El dado? ¿El tablero? Si empiezas por todo a la vez, una hora después tienes mucho código que todavía no funciona.
Ayuda hacerle al juego cinco preguntas antes de escribir código.
La idea
Cinco preguntas para cada juego
- Estado. ¿Qué números describen el mundo ahora mismo? En la carrera son la casilla de la ficha y el número del turno. En el ajedrez es dónde está cada pieza y a quién le toca.
- Reglas. ¿Qué está permitido y qué pasa después? «La ficha avanza tantas casillas como marca el dado.»
- Entrada. ¿Qué llega de fuera? La elección del jugador o el número del dado.
- Bucle. Turno tras turno: entrada, actualizar el estado, mostrarlo. Y vuelta a empezar.
- Final. ¿Cuándo termina el juego y quién gana? «La ficha está en la casilla 20.»
Las respuestas son casi el código terminado: el estado son variables, las reglas son funciones, la entrada es ask() o un generador de números aleatorios, el bucle es while o for, y el final es la condición del bucle.
Míralo
Turno tras turno
const random = makeRandom(42);let field = 0;for (let turn = 1; field < 20; turn++) { const roll = Math.floor(random() * 6) + 1; field = Math.min(field + roll, 20); console.log(`Turno ${turn}: dado ${roll}, casilla ${field}`);}console.log("¡Meta! La ficha llegó a la casilla 20.");Paso a paso, en palabras
- Un generador con semilla 42 y una ficha en la casilla 0.
- Turno 1: la entrada es una tirada de 2, el estado pasa a ser la casilla 2, la pantalla muestra la línea.
- Turno 2: la entrada es una tirada de 6, el estado pasa a ser la casilla 8, la pantalla muestra la línea.
- Turno 3: la entrada es una tirada de 1, el estado pasa a ser la casilla 9, la pantalla muestra la línea.
- Turno 4: la entrada es una tirada de 5, el estado pasa a ser la casilla 14, la pantalla muestra la línea.
- Turno 5: la entrada es una tirada de 1, el estado pasa a ser la casilla 15, la pantalla muestra la línea.
- Turno 6: la entrada es una tirada de 4, el estado pasa a ser la casilla 19, la pantalla muestra la línea.
- Turno 7: la entrada es una tirada de 1, el estado pasa a ser la casilla 20, la pantalla muestra la línea. Casilla 20: la condición field < 20 ahora es falsa.
- El bucle termina porque la casilla llegó a 20. Ese es el final del juego.
Todo el juego es un solo bucle. En cada turno entra una entrada (el dado), se actualiza el estado (la casilla nueva) y se muestra (una línea en la pantalla). La condición del bucle es el final: mientras la casilla sea menor que 20, hay otro turno.
Math.min mantiene la ficha dentro del tablero: si el dado la manda más allá de 20, se queda en 20.
Más precisamente, para curiosos
- Este juego es por turnos: no pasa nada hasta que llega la siguiente entrada. El ajedrez, los juegos de cartas y la mayoría de los juegos de esta sección funcionan así.
- Los juegos en tiempo real (disparos, carreras de coches) también tienen un bucle, pero no espera al jugador. Se ejecuta 60 veces por segundo: lee las teclas que están pulsadas en ese momento, mueve todo según el tiempo que ha pasado y dibuja el fotograma. Aunque el jugador no pulse nada, el mundo se sigue moviendo.
La idea
Misma semilla, mismo juego
Si las tiradas son realmente aleatorias, cada ejecución es un juego distinto. Entonces, ¿cómo compruebas que el juego funciona? No sabes qué esperar.
Math.random() no sirve: no puedes darle un número inicial. Por eso en estas lecciones el generador es una pequeña función makeRandom(seed) que recibe un número inicial, una semilla. Devuelve una función que, cada vez que la llamas, da el siguiente número de 0 a 1 (sin incluir el 1), igual que Math.random(). La misma semilla da la misma secuencia, siempre.
Por ahora copia makeRandom tal cual, como ask(). Dentro hay operaciones de bits que no vamos a necesitar. El dado es Math.floor(random() * 6) + 1: un número de 0 a 6 (sin incluir el 6), recortado a un entero, más 1.
Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++) { int a = first.nextInt(6) + 1; int b = second.nextInt(6) + 1; System.out.println(a + " y " + b);}3 y 34 y 41 y 13 y 31 y 1Random first = new Random(42);Random second = new Random(42);for (int i = 0; i < 5; i++){ int a = first.Next(1, 7); int b = second.Next(1, 7); Console.WriteLine(a + " y " + b);}5 y 51 y 11 y 14 y 42 y 2from random import Random
first = Random(42)second = Random(42)for i in range(5): a = first.randint(1, 6) b = second.randint(1, 6) print(f"{a} y {b}")6 y 61 y 11 y 16 y 63 y 3function makeRandom(seed) { return () => { seed = (seed + 0x9e3779b9) | 0; let z = seed; z = Math.imul(z ^ (z >>> 16), 0x85ebca6b); z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35); return ((z ^ (z >>> 16)) >>> 0) / 4294967296; };}
const first = makeRandom(42);const second = makeRandom(42);for (let i = 0; i < 5; i++) { const a = Math.floor(first() * 6) + 1; const b = Math.floor(second() * 6) + 1; console.log(`${a} y ${b}`);}2 y 26 y 61 y 15 y 51 y 1Los dos generadores con 42 dan los mismos números. Ejecuta el programa otra vez y volverán a ser iguales. Por eso los ejemplos y los ejercicios de aquí tienen una salida exacta que se puede comprobar.
Mira también la salida en las otras pestañas. Cada lenguaje tiene su propio generador: con la misma semilla dan números distintos. Por eso los ejemplos con números aleatorios tienen una salida para cada lenguaje.
Más precisamente, para curiosos
- Cuando el juego está listo para jugadores de verdad, cambias el generador:
const random = Math.random;. El resto del código no cambia, porque los dos se llaman igual,random(). Entonces cada partida es distinta. makeRandomno forma parte de la biblioteca estándar. En un proyecto real, un generador con semilla viene de un paquete, comoseedrandom.
- Es habitual imprimir la semilla al empezar la partida. Si un jugador encuentra un error, puedes jugar exactamente la misma partida con su semilla.
La idea
Primero la versión más pequeña que funciona
El programa de la escena es un juego completo en unas diez líneas. No tiene segundo jugador, ni menú, ni más reglas que «avanzar». Pero funciona de principio a fin y demuestra que el bucle y el final están bien.
A partir de ahí el juego crece una regla cada vez. La segunda regla: la ficha tiene que caer justo en la casilla 20. Si el dado la manda más allá, rebota hacia atrás tantas casillas como se pasó. Desde la 17 con un 5 llega a 20 y retrocede hasta la 18.
Solo añades una regla nueva cuando la anterior funciona. Si algo se rompe, sabes que está en lo último que escribiste.
La idea
La lógica devuelve, la pantalla imprime
La regla «justo 20» vive en una función que devuelve la casilla nueva. El final es una función que devuelve true o false:
static int move(int field, int roll) { int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static boolean isFinished(int field) { return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!isFinished(field)) { turn++; int roll = random.nextInt(6) + 1; field = move(field, roll); System.out.println("Turno " + turn + ": dado " + roll + ", casilla " + field);}System.out.println("Justo 20 tras " + turn + " turnos");Turno 1: dado 5, casilla 5Turno 2: dado 3, casilla 8Turno 3: dado 4, casilla 12Turno 4: dado 5, casilla 17Turno 5: dado 5, casilla 18Turno 6: dado 5, casilla 17Turno 7: dado 5, casilla 18Turno 8: dado 6, casilla 16Turno 9: dado 1, casilla 17Turno 10: dado 1, casilla 18Turno 11: dado 1, casilla 19Turno 12: dado 1, casilla 20Justo 20 tras 12 turnosstatic int Move(int field, int roll){ int next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
static bool IsFinished(int field){ return field == 20;}
Random random = new Random(7);int field = 0;int turn = 0;while (!IsFinished(field)){ turn++; int roll = random.Next(1, 7); field = Move(field, roll); Console.WriteLine("Turno " + turn + ": dado " + roll + ", casilla " + field);}Console.WriteLine("Justo 20 tras " + turn + " turnos");Turno 1: dado 3, casilla 3Turno 2: dado 6, casilla 9Turno 3: dado 4, casilla 13Turno 4: dado 1, casilla 14Turno 5: dado 3, casilla 17Turno 6: dado 5, casilla 18Turno 7: dado 1, casilla 19Turno 8: dado 6, casilla 15Turno 9: dado 6, casilla 19Turno 10: dado 6, casilla 15Turno 11: dado 3, casilla 18Turno 12: dado 6, casilla 16Turno 13: dado 3, casilla 19Turno 14: dado 1, casilla 20Justo 20 tras 14 turnosdef move(field, roll): new_field = field + roll if new_field > 20: return 20 - (new_field - 20) return new_field
def is_finished(field): return field == 20
random = Random(7)field = 0turn = 0while not is_finished(field): turn += 1 roll = random.randint(1, 6) field = move(field, roll) print(f"Turno {turn}: dado {roll}, casilla {field}")print(f"Justo 20 tras {turn} turnos")Turno 1: dado 3, casilla 3Turno 2: dado 2, casilla 5Turno 3: dado 4, casilla 9Turno 4: dado 6, casilla 15Turno 5: dado 1, casilla 16Turno 6: dado 1, casilla 17Turno 7: dado 5, casilla 18Turno 8: dado 1, casilla 19Turno 9: dado 3, casilla 18Turno 10: dado 5, casilla 17Turno 11: dado 1, casilla 18Turno 12: dado 5, casilla 17Turno 13: dado 2, casilla 19Turno 14: dado 1, casilla 20Justo 20 tras 14 turnosfunction move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
const random = makeRandom(7);let field = 0;let turn = 0;while (!isFinished(field)) { turn++; const roll = Math.floor(random() * 6) + 1; field = move(field, roll); console.log(`Turno ${turn}: dado ${roll}, casilla ${field}`);}console.log(`Justo 20 tras ${turn} turnos`);Turno 1: dado 1, casilla 1Turno 2: dado 3, casilla 4Turno 3: dado 6, casilla 10Turno 4: dado 4, casilla 14Turno 5: dado 3, casilla 17Turno 6: dado 2, casilla 19Turno 7: dado 5, casilla 16Turno 8: dado 2, casilla 18Turno 9: dado 2, casilla 20Justo 20 tras 9 turnosNi move ni isFinished imprimen nada. Solo calculan. La impresión está en el código que hay debajo de las funciones.
¿Por qué así? Una función que devuelve un número se puede llamar con los valores que quieras y comprobar el resultado: move(17, 5) tiene que dar 18. Una función que imprime solo se comprueba a ojo. Y si mañana el juego pasa de la consola a una ventana o al navegador, las reglas siguen siendo las mismas; solo cambia la pantalla.
Pruébalo
¿Dónde caerá la ficha?
¿Qué imprimirá el programa?
function move(field, roll) { const next = field + roll; if (next > 20) { return 20 - (next - 20); } return next;}
function isFinished(field) { return field === 20;}
console.log(move(10, 4));console.log(move(17, 3));console.log(move(18, 5));10 + 4 = 14. 17 + 3 = 20, justo. 18 + 5 = 23 se pasa 3 de 20, así que la ficha rebota hasta 20 − 3 = 17.
Pruébalo
Encuentra el error
La ficha debería avanzar, pero el programa imprime la casilla 0 en cada turno:
Turno 1: dado 1, casilla 0Turno 2: dado 3, casilla 0Turno 3: dado 6, casilla 0¿En qué línea está el error?
Elige la línea con el error.
move devuelve la casilla nueva, pero el resultado se tira. La función no puede cambiar el field de quien la llama: solo recibe el valor 0, no el field de quien la llama. Necesitas field = move(field, roll);. Si una línea así está dentro de while (!isFinished(field)), el juego no termina nunca.Pruébalo
¿Cuándo termina el juego?
¿Qué falta para que el final sea justo en la casilla 20?
function isFinished(field) {
return field 20;
}=== compara. == también compara, pero antes puede convertir uno de los valores a otro tipo, por eso en JavaScript se escribe ===. >= también funcionará, porque move nunca devuelve más de 20. Cuidado con un solo =: return field = 20; no es un error sino una asignación. La función devuelve 20, y 20 dentro de un if cuenta como verdadero, así que el juego «termina» en el primer turno.Ejercicio
Tres juegos pequeños
Resuelve estos ejercicios en tu editor. La lógica va en funciones que devuelven un valor, y la impresión va en el código que hay debajo.
1. Una carrera para dos. Ana y Luis tiran por turnos, con makeRandom(3). Vale la regla «justo 20», con move de la lección. Sus casillas están en un array [0, 0]. Gana quien llegue primero a 20.
Ana: dado 6, casilla 6Luis: dado 2, casilla 2Ana: dado 2, casilla 8Luis: dado 3, casilla 5Ana: dado 3, casilla 11Luis: dado 1, casilla 6Ana: dado 1, casilla 12Luis: dado 2, casilla 8Ana: dado 3, casilla 15Luis: dado 1, casilla 9Ana: dado 2, casilla 17Luis: dado 4, casilla 13Ana: dado 4, casilla 19Luis: dado 6, casilla 19Ana: dado 1, casilla 20Ana gana2. Adivina el número. El programa elige en secreto un número del 1 al 100 con makeRandom(2026). El jugador escribe sus intentos, uno por línea. Después de cada uno, el programa dice «Más alto», «Más bajo» o «Correcto». Al final dice cuántos intentos hicieron falta. La pista sale de una función hint(guess, secret) que devuelve el texto.
5075888184868750: Más alto75: Más alto88: Más bajo81: Más alto84: Más alto86: Más alto87: CorrectoAdivinado en 7 intentosLa entrada es distinta en cada lenguaje, porque sus generadores eligen un número secreto distinto a partir de la misma semilla.
3. ¿Quién ganó al tres en raya? La primera línea es el número de tableros. Cada tablero son tres líneas de tres caracteres: X, O o . para una casilla vacía. Escribe una función winner(board) que devuelva X, O o . si no hay tres en línea en una fila, una columna o una diagonal.
El tablero puede ser un array con las tres líneas que leas: cada línea es un texto, y board[r][c] es el carácter de la fila r, columna c. JavaScript no tiene un tipo aparte para un solo carácter, como char en Java: un carácter es un texto de longitud 1.
3XOXOXOOOXXXOOOOX.XXOXXOOOXXX ganaO ganaSin ganadorMás ejercicios
- Trampas. La casilla 13 manda la ficha de vuelta a la casilla 5. Añade la regla a
movey vuelve a ejecutar la carrera. - Imprime la semilla. Convierte la semilla en una variable, imprímela al empezar y ejecuta el juego con tres semillas distintas.
- Tres en raya para dos. Los jugadores escriben la fila y la columna de su jugada. Después de cada jugada, muestra el tablero y usa
winnerpara comprobar si el juego terminó.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Arrays bidimensionales · Cadenas: trabajar con texto · Funciones · Parámetros y return
- Ejercicios sobre el tema: Hagamos un juego