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 métodos 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 métodos, la entrada es Scanner o Random, el bucle es while o for, y el final es la condición del bucle.
Míralo
Turno tras turno
Random random = new Random(42);int field = 0;for (int turn = 1; field < 20; turn++) { int roll = random.nextInt(6) + 1; field = Math.min(field + roll, 20); System.out.println("Turno " + turn + ": dado " + roll + ", casilla " + field);}System.out.println("¡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 3, el estado pasa a ser la casilla 3, la pantalla muestra la línea.
- Turno 2: la entrada es una tirada de 4, el estado pasa a ser la casilla 7, la pantalla muestra la línea.
- Turno 3: la entrada es una tirada de 1, el estado pasa a ser la casilla 8, la pantalla muestra la línea.
- Turno 4: la entrada es una tirada de 3, el estado pasa a ser la casilla 11, la pantalla muestra la línea.
- Turno 5: la entrada es una tirada de 1, el estado pasa a ser la casilla 12, la pantalla muestra la línea.
- Turno 6: la entrada es una tirada de 2, el estado pasa a ser la casilla 14, la pantalla muestra la línea.
- Turno 7: la entrada es una tirada de 6, 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.
Por eso en estas lecciones Random recibe un número inicial, una semilla. La misma semilla da la misma secuencia, siempre:
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, quitas la semilla:
new Random(). Entonces el generador elige una semilla por sí mismo a partir del reloj, y cada partida es distinta.
- 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 un método que devuelve la casilla nueva. El final es un método 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 main.
¿Por qué así? Un método que devuelve un número se puede llamar con los valores que quieras y comprobar el resultado: move(17, 5) tiene que dar 18. Un método que imprime solo se comprueba a ojo. Y si mañana el juego pasa de la consola a una ventana, las reglas siguen siendo las mismas; solo cambia la pantalla.
Pruébalo
¿Dónde caerá la ficha?
¿Qué imprimirá el programa?
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;}
System.out.println(move(10, 4));System.out.println(move(17, 3));System.out.println(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 5, casilla 0Turno 2: dado 3, casilla 0Turno 3: dado 4, 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. El método no puede cambiar el field de quien lo llama: el parámetro es una copia del valor. 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?
static boolean isFinished(int field) {
return field 20;
}== compara. Un solo = es una asignación y aquí no compila. >= también funcionará, porque move nunca devuelve más de 20, pero == dice la regla con más claridad: el final es justo en 20.Ejercicio
Tres juegos pequeños
Resuelve estos ejercicios en tu editor. La lógica va en métodos que devuelven un valor, y la impresión va en main.
1. Una carrera para dos. Ana y Luis tiran por turnos, con new Random(3). Vale la regla «justo 20», con move de la lección. Sus casillas están en un array int[2]. Gana quien llegue primero a 20.
Ana: dado 3, casilla 3Luis: dado 3, casilla 3Ana: dado 1, casilla 4Luis: dado 2, casilla 5Ana: dado 1, casilla 5Luis: dado 1, casilla 6Ana: dado 4, casilla 9Luis: dado 5, casilla 11Ana: dado 2, casilla 11Luis: dado 2, casilla 13Ana: dado 4, casilla 15Luis: dado 5, casilla 18Ana: dado 6, casilla 19Luis: dado 5, casilla 17Ana: dado 4, casilla 17Luis: dado 5, casilla 18Ana: dado 1, casilla 18Luis: dado 4, casilla 18Ana: dado 5, casilla 17Luis: dado 2, casilla 20Luis gana2. Adivina el número. El programa elige en secreto un número del 1 al 100 con new Random(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 un método String hint(int guess, int secret) que devuelve el texto.
50758894979910050: Más alto75: Más alto88: Más alto94: Más alto97: Más alto99: Más alto100: 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 un método char winner(char[][] board) que devuelva X, O o . si no hay tres en línea en una fila, una columna o una diagonal.
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 · Métodos · Parámetros y return
- Ejercicios sobre el tema: Hagamos un juego