Ir al contenido
wannacode

El laboratorio cerrado 2: acertijos y la salida

En resumen: El final del juego de escape: una puerta cerrada con llave, una cerradura de código armada con pistas, ganar con un conteo de movimientos y un código de guardado que el juego imprime y luego vuelve a leer.

Problema

Todas las puertas están abiertas

En la primera lección el laboratorio está cerrado solo de palabra: el jugador va adonde quiere. La llave y la nota están ahí tiradas y no sirven para nada. El juego no tiene final.

Un juego de escape necesita:

  • una puerta cerrada con llave, que se abre con un objeto;
  • una cerradura de código, cuyo código armas con pistas de distintas salas;
  • un final: el jugador está afuera, con un conteo de movimientos para presumir;
  • un código de guardado: el juego es largo, y el jugador quiere parar y seguir después.

El código de guardado es un texto corto. El juego lo imprime, y el jugador lo escribe de vuelta. Si el texto está dañado, el juego tiene que decirlo, no romperse.

La idea

Una puerta cerrada con llave

La sala recibe una cerradura. El secreto es el nombre del objeto que la abre, o el código:

private String secret = null;
private boolean codeLock = false;
private boolean locked = false;
public void lockWithKey(String keyName) {
secret = keyName;
codeLock = false;
locked = true;
}
public void lockWithCode(String code) {
secret = code;
codeLock = true;
locked = true;
}
public boolean isLocked() {
return locked;
}
public boolean isCodeLock() {
return codeLock;
}
public boolean opensWith(String attempt) {
return locked && secret.equals(attempt);
}
public void unlock() {
locked = false;
}

En Python los campos se crean en __init__, por eso se muestra otra vez, con los tres campos nuevos al final.

Ahora «ir» se detiene ante una puerta cerrada:

class GoCommand implements Command {
@Override
public String execute(Game game, String argument) {
Room next = game.getCurrent().getExit(argument);
if (next == null) {
return "Por ahí no hay salida.";
}
if (next.isLocked()) {
return "La puerta hacia «" + next.getName() + "» está cerrada con llave.";
}
game.setCurrent(next);
return next.describe();
}
}

En el mundo, la oficina se cierra con la llave del almacén, y el nuevo vestíbulo con un código:

Room foyer = new Room("vestíbulo", "La puerta de vidrio que da a la calle está abierta.");
corridor.addExit("norte", foyer);
foyer.addExit("sur", corridor);
office.lockWithKey("llave");
foyer.lockWithCode("4719");

La idea

La llave abre

«usar llave» revisa la mochila y luego busca una puerta vecina cerrada que se abra con ese objeto:

class UseCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (!game.has(argument)) {
return "No tienes " + argument + ".";
}
for (Room room : game.getCurrent().getNeighbours()) {
if (!room.isCodeLock() && room.opensWith(argument)) {
game.unlock(room);
return "Abres la puerta hacia «" + room.getName() + "».";
}
}
return "Aquí «" + argument + "» no sirve.";
}
}

Abrir la puerta pasa por el juego, no directamente por la sala. Así el juego recuerda qué puertas están abiertas. Lo vas a necesitar para el código de guardado.

Pruébalo

¿Qué llave, qué puerta?

El jugador escribe estos comandos desde el inicio del juego. ¿Qué responde el juego a los tres comandos «usar llave», en orden?

ir norte
usar llave
ir este
tomar llave
usar llave
ir oeste
usar llave
Respuesta

La idea

Una cerradura de código hecha de pistas

El vestíbulo se cierra con cuatro dígitos. El juego no los dice directamente:

  • la nota de la oficina: «Código del vestíbulo: 47 y el día del inventario.»;
  • el calendario del almacén: «Inventario el 19 de octubre».

El código es 4719. El comando «ingresar» lo compara con la cerradura de código de una puerta vecina:

class CodeCommand implements Command {
@Override
public String execute(Game game, String argument) {
for (Room room : game.getCurrent().getNeighbours()) {
if (room.isCodeLock() && room.isLocked()) {
if (room.opensWith(argument)) {
game.unlock(room);
return "La cerradura hace clic. La puerta hacia «" + room.getName() + "» está abierta.";
}
return "Código incorrecto. La cerradura parpadea en rojo.";
}
}
return "Aquí no hay cerradura de código.";
}
}

El acertijo está en los datos, no en el código: otro código y otras pistas hacen otro juego con las mismas clases.

Míralo

Afuera

El juego se gana cuando el jugador llega al vestíbulo:

public boolean isWon() {
return current.getName().equals("vestíbulo");
}

handle cuenta como movimiento cada comando que entiende. El bucle en el programa se detiene cuando ganas:

Game game = new Game();
System.out.println(game.getCurrent().describe());
Scanner in = new Scanner(System.in);
while (!game.isWon() && in.hasNextLine()) {
String line = in.nextLine();
System.out.println("> " + line);
System.out.println(game.handle(line));
}
if (game.isWon()) {
System.out.println("Lograste salir en " + game.getMoves() + " movimientos.");
}
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir oeste
La puerta hacia «oficina» está cerrada con llave.
> ir este
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Ves: llave
Salidas: oeste
> tomar llave
Tomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».
> ir oeste
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> usar llave
Abres la puerta hacia «oficina».
> ir oeste
[oficina] Un escritorio, una silla y un archivero lleno de carpetas.
Ves: nota
Salidas: este
> tomar nota
Tomas: nota. Código del vestíbulo: 47 y el día del inventario.
> ir este
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir norte
La puerta hacia «vestíbulo» está cerrada con llave.
> ingresar 1234
Código incorrecto. La cerradura parpadea en rojo.
> ingresar 4719
La cerradura hace clic. La puerta hacia «vestíbulo» está abierta.
> ir norte
[vestíbulo] La puerta de vidrio que da a la calle está abierta.
Salidas: sur
Lograste salir en 13 movimientos.

Un for sobre sys.stdin no tiene una condición que pueda comprobar la victoria. Por eso la comprobación está al final del cuerpo, con break.

Con esta entrada:

Entrada
ir norte
ir oeste
ir este
tomar llave
ir oeste
usar llave
ir oeste
tomar nota
ir este
ir norte
ingresar 1234
ingresar 4719
ir norte

game = Game()
print(game.current.describe())
for line in sys.stdin:
line = line.rstrip("\n")
print(f"> {line}")
print(game.handle(line))
if game.is_won():
break
if game.is_won():
print(f"Lograste salir en {game.moves} movimientos.")
Paso a paso, en palabras
  1. Empiezas en «laboratorio». La puerta hacia «oficina» se abre con «llave», y la puerta hacia «vestíbulo» con un código de cuatro dígitos.
  2. Movimiento 1: «ir norte». Entras en «pasillo».
  3. Movimiento 2: «ir oeste». La puerta hacia «oficina» está cerrada con llave: «ir» se detiene antes de moverte.
  4. Movimiento 3: «ir este». Entras en «almacén». En la pared: «Inventario el 19 de octubre».
  5. Movimiento 4: «tomar llave». Tomas: llave. Abre la puerta hacia «oficina».
  6. Movimiento 5: «ir oeste». Entras en «pasillo».
  7. Movimiento 6: «usar llave». «llave» está en la mochila y abre la puerta vecina cerrada hacia «oficina».
  8. Movimiento 7: «ir oeste». Entras en «oficina».
  9. Movimiento 8: «tomar nota». Tomas: nota. Dice: «Código del vestíbulo: 47 y el día del inventario.»
  10. Movimiento 9: «ir este». Entras en «pasillo».
  11. Movimiento 10: «ir norte». La puerta hacia «vestíbulo» está cerrada con llave: «ir» se detiene antes de moverte.
  12. Movimiento 11: «ingresar 1234». 1234 no es el código. La cerradura parpadea en rojo.
  13. Movimiento 12: «ingresar 4719». 47 de la nota y 19 del calendario: 4719. La cerradura de «vestíbulo» hace clic.
  14. Movimiento 13: «ir norte». Entras en «vestíbulo».
  15. Llegas a «vestíbulo»: is_won() es True, y break detiene el bucle. Sales en 13 movimientos.

La idea

Un código de guardado

Todo el estado del juego son cuatro cosas: dónde está el jugador, qué lleva, qué puertas abrió y cuántos movimientos jugó. Van a un texto corto, separadas por ;:

public String saveCode() {
List<String> names = new ArrayList<>();
for (Item item : inventory) {
names.add(item.getName());
}
String items = names.isEmpty() ? "-" : String.join(",", names);
String doors = unlocked.isEmpty() ? "-" : String.join(",", unlocked);
return current.getName() + ";" + items + ";" + doors + ";" + moves;
}

Por ejemplo oficina;llave,nota;oficina;9. Las listas usan comas, y una lista vacía es -, así que nunca queda un hueco vacío entre dos signos ;.

El juego imprime el código, y el jugador lo escribe de vuelta con «cargar». No necesitas un archivo: el código se puede copiar en un papel o mandar a un amigo.

Más precisamente, para curiosos

También puedes escribir el mismo texto en un archivo, con lo que aprendiste en el módulo «Texto y archivos»:

from pathlib import Path
Path("save.txt").write_text(game.save_code(), encoding="utf-8")
...
later.load(Path("save.txt").read_text(encoding="utf-8"))

La idea

Un código dañado

El jugador puede equivocarse en una letra o intentar hacer trampa: aparecer en la oficina cerrada. Entonces la carga lanza su propia excepción:

class InvalidSaveCodeException extends Exception {
public InvalidSaveCodeException(String message) {
super(message);
}
}

load construye un mundo nuevo y comprueba el código parte por parte sobre él. Solo cuando todo ha pasado, el mundo nuevo se vuelve el actual:

public void load(String code) throws InvalidSaveCodeException {
String[] parts = code.split(";");
if (parts.length != 4) {
throw new InvalidSaveCodeException("necesita 4 partes separadas por ;");
}
Map<String, Room> world = buildWorld();
Room room = world.get(parts[0]);
if (room == null) {
throw new InvalidSaveCodeException("no existe la sala " + parts[0]);
}
List<Item> items = new ArrayList<>();
if (!parts[1].equals("-")) {
for (String name : parts[1].split(",")) {
Item found = takeFrom(world, name);
if (found == null) {
throw new InvalidSaveCodeException("no existe el objeto " + name);
}
items.add(found);
}
}
List<String> doors = new ArrayList<>();
if (!parts[2].equals("-")) {
for (String name : parts[2].split(",")) {
Room door = world.get(name);
if (door == null || !door.isLocked()) {
throw new InvalidSaveCodeException("no hay ninguna puerta cerrada hacia " + name);
}
door.unlock();
doors.add(name);
}
}
if (room.isLocked()) {
throw new InvalidSaveCodeException("la sala " + room.getName() + " está cerrada con llave");
}
int savedMoves;
try {
savedMoves = Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
throw new InvalidSaveCodeException("los movimientos no son un número: " + parts[3]);
}
rooms = world;
current = room;
inventory = items;
unlocked = doors;
moves = savedMoves;
}
private Item takeFrom(Map<String, Room> world, String name) {
for (Room room : world.values()) {
Item found = room.takeItem(name);
if (found != null) {
return found;
}
}
return null;
}

El comando «cargar» captura la excepción y le dice al jugador qué está mal:

class LoadCommand implements Command {
@Override
public String execute(Game game, String argument) {
try {
game.load(argument);
return "Continúas donde lo dejaste.\n" + game.getCurrent().describe();
} catch (InvalidSaveCodeException e) {
return "El código no sirve: " + e.getMessage() + ".";
}
}
}
Más precisamente, para curiosos
  • int lanza ValueError, y load la convierte en su propia excepción. Así «cargar» captura una sola excepción, y el mensaje está en el idioma del juego.
for text in ["cuatro", "", "7"]:
try:
print(int(text))
except ValueError as e:
print(e)
Salida
invalid literal for int() with base 10: 'cuatro'
invalid literal for int() with base 10: ''
7
  • El código no protege contra las trampas: quien descubra el formato puede escribir «vestíbulo». La comprobación de que la sala no está cerrada solo protege de lo evidente. Los juegos de verdad añaden una suma de control: un número calculado a partir de todo el texto, que no coincide si alguien lo ha tocado.

Pruébalo

Encuentra el error

El jugador escribe cargar oficina;martillo;-;3. El juego dice «El código no sirve: no existe el objeto martillo.», pero después de «mirar» el jugador está en la oficina, que está cerrada. ¿En qué línea está el error?

Elige la línea con el error.

Míralo

Guarda y continúa

El jugador toma la llave y guarda. Luego prueba tres códigos dañados y por fin uno que funciona:

Entrada
ir norte
ir este
tomar llave
guardar
cargar almacén;martillo;-;4
cargar pasillo;llave;oficina;cuatro
cargar oficina;llave;-;7
cargar oficina;llave;oficina;7
ir este
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir este
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Ves: llave
Salidas: oeste
> tomar llave
Tomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».
> guardar
Código de guardado: almacén;llave;-;4
> cargar almacén;martillo;-;4
El código no sirve: no existe el objeto martillo.
> cargar pasillo;llave;oficina;cuatro
El código no sirve: los movimientos no son un número: cuatro.
> cargar oficina;llave;-;7
El código no sirve: la sala oficina está cerrada con llave.
> cargar oficina;llave;oficina;7
Continúas donde lo dejaste.
[oficina] Un escritorio, una silla y un archivero lleno de carpetas.
Ves: nota
Salidas: este
> ir este
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste

El último código se escribió a mano, pero es válido: la oficina está abierta y la llave está en la mochila. El juego no sabe si imprimió el código él mismo. Solo sabe que es un estado posible.

Ejercicio

Más acertijos

Resuelve las tareas en tu editor, con las clases de la lección.

1. El sótano oscuro. Al sur del laboratorio hay un sótano («Un sótano bajo con tuberías a lo largo del techo.»), y en él un fusible de repuesto. Solo se entra con una linterna en la mochila. Pista: un comando que extiende GoCommand comprueba si es el sótano y llama al padre con super().execute(...). Regístralo con game.add_command en lugar de «ir».

Entrada
ir sur
tomar linterna
ir sur
tomar fusible
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte, sur
> ir sur
Está demasiado oscuro. Sin luz no encontrarás nada.
> tomar linterna
Tomas: linterna. Funciona y la batería está casi llena.
> ir sur
[sótano] Un sótano bajo con tuberías a lo largo del techo.
Ves: fusible
Salidas: norte
> tomar fusible
Tomas: fusible. Un fusible de repuesto para la caja de fusibles.

2. Un temporizador. Los guardias llegan después de 12 movimientos. Si el jugador no está afuera para entonces, el juego termina.

Entrada
ir norte
ir este
tomar llave
ir oeste
ir oeste
usar llave
ir oeste
tomar nota
ir este
ir este
ir oeste
ingresar 4719
ir norte
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir este
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Ves: llave
Salidas: oeste
> tomar llave
Tomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».
> ir oeste
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir oeste
La puerta hacia «oficina» está cerrada con llave.
> usar llave
Abres la puerta hacia «oficina».
> ir oeste
[oficina] Un escritorio, una silla y un archivero lleno de carpetas.
Ves: nota
Salidas: este
> tomar nota
Tomas: nota. Código del vestíbulo: 47 y el día del inventario.
> ir este
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir este
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Salidas: oeste
> ir oeste
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ingresar 4719
La cerradura hace clic. La puerta hacia «vestíbulo» está abierta.
Llegaron los guardias. Te atraparon tras 12 movimientos.

3. Una pista por un movimiento. Un comando «pista» dice el siguiente paso según el estado: la llave, la oficina, la nota, el calendario. Cuesta un movimiento, como cualquier comando.

Entrada
pista
ir norte
ir este
tomar llave
pista
ir oeste
ir oeste
usar llave
pista
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> pista
Pista: revisa el almacén.
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir este
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Ves: llave
Salidas: oeste
> tomar llave
Tomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».
> pista
Pista: la llave es para la oficina.
> ir oeste
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: norte, sur, este, oeste
> ir oeste
La puerta hacia «oficina» está cerrada con llave.
> usar llave
Abres la puerta hacia «oficina».
> pista
Pista: hay algo que leer en la oficina.

Más tareas

  • El fusible. La cerradura de código no funciona hasta que el fusible del sótano se haya «usado» en el pasillo.
  • Los movimientos en el código. Haz que el temporizador funcione también después de «cargar»: el código ya lleva los movimientos.
  • Una suma de control. Añade una quinta parte: la suma de las longitudes de las cuatro primeras. Un código con una suma incorrecta está dañado.

Test

Ponte a prueba

1. El jugador está en el almacén y escribe «usar llave». La llave está en la mochila. ¿Qué pasa?
2. El código de guardado es «pasillo;llave,nota;oficina;9». ¿Qué dice la tercera parte?
3. ¿Por qué load comprueba todo sobre un mundo nuevo y solo al final reemplaza el juego actual?
4. ¿Por qué el conteo de movimientos sube después de _find y no antes?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·