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;}private string secret = null;
public bool IsLocked { get; private set; } = false;public bool IsCodeLock { get; private set; } = false;
public void LockWithKey(string keyName){ secret = keyName; IsCodeLock = false; IsLocked = true;}
public void LockWithCode(string code){ secret = code; IsCodeLock = true; IsLocked = true;}
public bool OpensWith(string attempt){ return IsLocked && secret == attempt;}
public void Unlock(){ IsLocked = false;}def __init__(self, name, description): self._name = name self._description = description self._exits = {} self._items = [] self._secret = None self._code_lock = False self._locked = False
def lock_with_key(self, key_name): self._secret = key_name self._code_lock = False self._locked = True
def lock_with_code(self, code): self._secret = code self._code_lock = True self._locked = True
@propertydef is_locked(self): return self._locked
@propertydef is_code_lock(self): return self._code_lock
def opens_with(self, attempt): return self._locked and self._secret == attempt
def unlock(self): self._locked = False#secret = null;#codeLock = false;#locked = false;
lockWithKey(keyName) { this.#secret = keyName; this.#codeLock = false; this.#locked = true;}
lockWithCode(code) { this.#secret = code; this.#codeLock = true; this.#locked = true;}
get isLocked() { return this.#locked;}
get isCodeLock() { return this.#codeLock;}
opensWith(attempt) { return this.#locked && this.#secret === attempt;}
unlock() { this.#locked = false;}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(); }}class GoCommand : ICommand{ public string Execute(Game game, string argument) { Room next = game.Current.GetExit(argument); if (next == null) { return "Por ahí no hay salida."; } if (next.IsLocked) { return "La puerta hacia «" + next.Name + "» está cerrada con llave."; } game.Current = next; return next.Describe(); }}class GoCommand(Command): def execute(self, game, argument): next_room = game.current.get_exit(argument) if next_room is None: return "Por ahí no hay salida." if next_room.is_locked: return f"La puerta hacia «{next_room.name}» está cerrada con llave." game.current = next_room return next_room.describe()class GoCommand { execute(game, argument) { const next = game.current.getExit(argument); if (next === undefined) { return "Por ahí no hay salida."; } if (next.isLocked) { return `La puerta hacia «${next.name}» está cerrada con llave.`; } game.current = 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");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");foyer = Room("vestíbulo", "La puerta de vidrio que da a la calle está abierta.")corridor.add_exit("norte", foyer)foyer.add_exit("sur", corridor)office.lock_with_key("llave")foyer.lock_with_code("4719")const 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."; }}class UseCommand : ICommand{ public string Execute(Game game, string argument) { if (!game.Has(argument)) { return "No tienes " + argument + "."; } foreach (Room room in game.Current.GetNeighbours()) { if (!room.IsCodeLock && room.OpensWith(argument)) { game.Unlock(room); return "Abres la puerta hacia «" + room.Name + "»."; } } return "Aquí «" + argument + "» no sirve."; }}class UseCommand(Command): def execute(self, game, argument): if not game.has(argument): return f"No tienes {argument}." for room in game.current.get_neighbours(): if not room.is_code_lock and room.opens_with(argument): game.unlock(room) return f"Abres la puerta hacia «{room.name}»." return f"Aquí «{argument}» no sirve."class UseCommand { execute(game, argument) { if (!game.has(argument)) { return `No tienes ${argument}.`; } for (const room of game.current.getNeighbours()) { if (!room.isCodeLock && room.opensWith(argument)) { game.unlock(room); return `Abres la puerta hacia «${room.name}».`; } } 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 norteusar llaveir estetomar llaveusar llaveir oesteusar llaveLa primera vez, la llave todavía está en el almacén. La segunda vez está en la mochila, pero desde el almacén no se ve la oficina: el único vecino es el pasillo. La tercera vez, el jugador está en el pasillo, y la puerta del oeste está cerrada justo con esta llave.
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."; }}class CodeCommand : ICommand{ public string Execute(Game game, string argument) { foreach (Room room in game.Current.GetNeighbours()) { if (room.IsCodeLock && room.IsLocked) { if (room.OpensWith(argument)) { game.Unlock(room); return "La cerradura hace clic. La puerta hacia «" + room.Name + "» está abierta."; } return "Código incorrecto. La cerradura parpadea en rojo."; } } return "Aquí no hay cerradura de código."; }}class CodeCommand(Command): def execute(self, game, argument): for room in game.current.get_neighbours(): if room.is_code_lock and room.is_locked: if room.opens_with(argument): game.unlock(room) return f"La cerradura hace clic. La puerta hacia «{room.name}» está abierta." return "Código incorrecto. La cerradura parpadea en rojo." return "Aquí no hay cerradura de código."class CodeCommand { execute(game, argument) { for (const room of game.current.getNeighbours()) { if (room.isCodeLock && room.isLocked) { if (room.opensWith(argument)) { game.unlock(room); return `La cerradura hace clic. La puerta hacia «${room.name}» 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");}public bool IsWon(){ return Current.Name == "vestíbulo";}def is_won(self): return self._current.name == "vestíbulo"isWon() { return this.#current.name === "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.");}[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte> ir norte[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oeste> ir oesteLa 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: llaveSalidas: oeste> tomar llaveTomas: 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 llaveAbres la puerta hacia «oficina».> ir oeste[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> tomar notaTomas: 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 norteLa puerta hacia «vestíbulo» está cerrada con llave.> ingresar 1234Código incorrecto. La cerradura parpadea en rojo.> ingresar 4719La 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: surLograste salir en 13 movimientos.Game game = new Game();Console.WriteLine(game.Current.Describe());string line = Console.ReadLine();while (!game.IsWon() && line != null){ Console.WriteLine("> " + line); Console.WriteLine(game.Handle(line)); line = Console.ReadLine();}if (game.IsWon()){ Console.WriteLine("Lograste salir en " + game.Moves + " movimientos.");}[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte> ir norte[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oeste> ir oesteLa 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: llaveSalidas: oeste> tomar llaveTomas: 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 llaveAbres la puerta hacia «oficina».> ir oeste[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> tomar notaTomas: 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 norteLa puerta hacia «vestíbulo» está cerrada con llave.> ingresar 1234Código incorrecto. La cerradura parpadea en rojo.> ingresar 4719La 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: surLograste salir en 13 movimientos.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(): breakif game.is_won(): print(f"Lograste salir en {game.moves} movimientos.")[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte> ir norte[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oeste> ir oesteLa 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: llaveSalidas: oeste> tomar llaveTomas: 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 llaveAbres la puerta hacia «oficina».> ir oeste[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> tomar notaTomas: 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 norteLa puerta hacia «vestíbulo» está cerrada con llave.> ingresar 1234Código incorrecto. La cerradura parpadea en rojo.> ingresar 4719La 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: surLograste salir en 13 movimientos.const game = new Game();console.log(game.current.describe());let line = await ask("");while (!game.isWon() && line !== undefined) { console.log(`> ${line}`); console.log(game.handle(line)); line = await ask("");}rl.close();if (game.isWon()) { console.log(`Lograste salir en ${game.moves} movimientos.`);}[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte> ir norte[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oeste> ir oesteLa 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: llaveSalidas: oeste> tomar llaveTomas: 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 llaveAbres la puerta hacia «oficina».> ir oeste[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> tomar notaTomas: 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 norteLa puerta hacia «vestíbulo» está cerrada con llave.> ingresar 1234Código incorrecto. La cerradura parpadea en rojo.> ingresar 4719La 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: surLograste salir en 13 movimientos.Con esta entrada:
ir norteir oesteir estetomar llaveir oesteusar llaveir oestetomar notair esteir norteingresar 1234ingresar 4719ir norteconst game = new Game();console.log(game.current.describe());let line = await ask("");while (!game.isWon() && line !== undefined) { console.log(`> ${line}`); console.log(game.handle(line)); line = await ask("");}rl.close();if (game.isWon()) { console.log(`Lograste salir en ${game.moves} movimientos.`);}Paso a paso, en palabras
- 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.
- Movimiento 1: «ir norte». Entras en «pasillo».
- Movimiento 2: «ir oeste». La puerta hacia «oficina» está cerrada con llave: «ir» se detiene antes de moverte.
- Movimiento 3: «ir este». Entras en «almacén». En la pared: «Inventario el 19 de octubre».
- Movimiento 4: «tomar llave». Tomas: llave. Abre la puerta hacia «oficina».
- Movimiento 5: «ir oeste». Entras en «pasillo».
- Movimiento 6: «usar llave». «llave» está en la mochila y abre la puerta vecina cerrada hacia «oficina».
- Movimiento 7: «ir oeste». Entras en «oficina».
- Movimiento 8: «tomar nota». Tomas: nota. Dice: «Código del vestíbulo: 47 y el día del inventario.»
- Movimiento 9: «ir este». Entras en «pasillo».
- Movimiento 10: «ir norte». La puerta hacia «vestíbulo» está cerrada con llave: «ir» se detiene antes de moverte.
- Movimiento 11: «ingresar 1234». 1234 no es el código. La cerradura parpadea en rojo.
- Movimiento 12: «ingresar 4719». 47 de la nota y 19 del calendario: 4719. La cerradura de «vestíbulo» hace clic.
- Movimiento 13: «ir norte». Entras en «vestíbulo».
- Llegas a «vestíbulo»: isWon() es true, y el bucle se detiene. 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;}public string SaveCode(){ List<string> names = new List<string>(); foreach (Item item in Inventory) { names.Add(item.Name); } string items = names.Count == 0 ? "-" : string.Join(",", names); string doors = unlocked.Count == 0 ? "-" : string.Join(",", unlocked); return Current.Name + ";" + items + ";" + doors + ";" + Moves;}def save_code(self): names = [] for item in self._inventory: names.append(item.name) items = ",".join(names) if names else "-" doors = ",".join(self._unlocked) if self._unlocked else "-" return f"{self._current.name};{items};{doors};{self._moves}"saveCode() { const names = []; for (const item of this.#inventory) { names.push(item.name); } const items = names.length === 0 ? "-" : names.join(","); const doors = this.#unlocked.length === 0 ? "-" : this.#unlocked.join(","); return `${this.#current.name};${items};${doors};${this.#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»:
import { readFileSync, writeFileSync } from "node:fs";
writeFileSync("save.txt", game.saveCode());...later.load(readFileSync("save.txt", "utf8"));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); }}class InvalidSaveCodeException : Exception{ public InvalidSaveCodeException(string message) : base(message) { }}class InvalidSaveCodeError(Exception): passclass InvalidSaveCodeError extends Error { constructor(message) { super(message); this.name = "InvalidSaveCodeError"; }}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;}public void Load(string code){ string[] parts = code.Split(';'); if (parts.Length != 4) { throw new InvalidSaveCodeException("necesita 4 partes separadas por ;"); } Dictionary<string, Room> world = BuildWorld(); if (!world.ContainsKey(parts[0])) { throw new InvalidSaveCodeException("no existe la sala " + parts[0]); } Room room = world[parts[0]]; List<Item> items = new List<Item>(); if (parts[1] != "-") { foreach (string name in 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 List<string>(); if (parts[2] != "-") { foreach (string name in parts[2].Split(',')) { if (!world.ContainsKey(name) || !world[name].IsLocked) { throw new InvalidSaveCodeException("no hay ninguna puerta cerrada hacia " + name); } world[name].Unlock(); doors.Add(name); } } if (room.IsLocked) { throw new InvalidSaveCodeException("la sala " + room.Name + " está cerrada con llave"); } int savedMoves; try { savedMoves = int.Parse(parts[3]); } catch (FormatException) { 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(Dictionary<string, Room> world, string name){ foreach (Room room in world.Values) { Item found = room.TakeItem(name); if (found != null) { return found; } } return null;}def load(self, code): parts = code.split(";") if len(parts) != 4: raise InvalidSaveCodeError("necesita 4 partes separadas por ;") world = self._build_world() room = world.get(parts[0]) if room is None: raise InvalidSaveCodeError(f"no existe la sala {parts[0]}") items = [] if parts[1] != "-": for name in parts[1].split(","): found = self._take_from(world, name) if found is None: raise InvalidSaveCodeError(f"no existe el objeto {name}") items.append(found) doors = [] if parts[2] != "-": for name in parts[2].split(","): door = world.get(name) if door is None or not door.is_locked: raise InvalidSaveCodeError(f"no hay ninguna puerta cerrada hacia {name}") door.unlock() doors.append(name) if room.is_locked: raise InvalidSaveCodeError(f"la sala {room.name} está cerrada con llave") try: saved_moves = int(parts[3]) except ValueError: raise InvalidSaveCodeError(f"los movimientos no son un número: {parts[3]}") self._rooms = world self._current = room self._inventory = items self._unlocked = doors self._moves = saved_moves
def _take_from(self, world, name): for room in world.values(): found = room.take_item(name) if found is not None: return found return Noneload(code) { const parts = code.split(";"); if (parts.length !== 4) { throw new InvalidSaveCodeError("necesita 4 partes separadas por ;"); } const world = this.#buildWorld(); const room = world.get(parts[0]); if (room === undefined) { throw new InvalidSaveCodeError(`no existe la sala ${parts[0]}`); } const items = []; if (parts[1] !== "-") { for (const name of parts[1].split(",")) { const found = this.#takeFrom(world, name); if (found === null) { throw new InvalidSaveCodeError(`no existe el objeto ${name}`); } items.push(found); } } const doors = []; if (parts[2] !== "-") { for (const name of parts[2].split(",")) { const door = world.get(name); if (door === undefined || !door.isLocked) { throw new InvalidSaveCodeError(`no hay ninguna puerta cerrada hacia ${name}`); } door.unlock(); doors.push(name); } } if (room.isLocked) { throw new InvalidSaveCodeError(`la sala ${room.name} está cerrada con llave`); } if (!/^\d+$/.test(parts[3])) { throw new InvalidSaveCodeError(`los movimientos no son un número: ${parts[3]}`); } this.#rooms = world; this.#current = room; this.#inventory = items; this.#unlocked = doors; this.#moves = Number(parts[3]);}
#takeFrom(world, name) { for (const room of world.values()) { const 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() + "."; } }}class LoadCommand : ICommand{ public string Execute(Game game, string argument) { try { game.Load(argument); return "Continúas donde lo dejaste.\n" + game.Current.Describe(); } catch (InvalidSaveCodeException e) { return "El código no sirve: " + e.Message + "."; } }}class LoadCommand(Command): def execute(self, game, argument): try: game.load(argument) return "Continúas donde lo dejaste.\n" + game.current.describe() except InvalidSaveCodeError as e: return f"El código no sirve: {e}."class LoadCommand { execute(game, argument) { try { game.load(argument); return `Continúas donde lo dejaste.\n${game.current.describe()}`; } catch (e) { if (!(e instanceof InvalidSaveCodeError)) { throw e; } return `El código no sirve: ${e.message}.`; } }}Más precisamente, para curiosos
-
Numberno lanza nada: un número dañado esNaN, y un texto vacío es0. Por esoloadcomprueba los movimientos con la expresión regular/^\d+$/(solo dígitos) y lanza ella misma su propia excepción. Así «cargar» captura una sola excepción, y el mensaje está en el idioma del juego.
console.log(Number("cuatro"));console.log(Number(""));console.log(Number("7"));console.log(/^\d+$/.test("cuatro"), /^\d+$/.test(""), /^\d+$/.test("7"));NaN07false false true- 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.
El jugador se mueve antes de que el código se haya comprobado por completo. Luego la comprobación de los objetos lanza una excepción, pero el movimiento ya ocurrió. La línea tiene que estar al final, junto con los demás cambios, después de todas las comprobaciones.
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:
ir norteir estetomar llaveguardarcargar almacén;martillo;-;4cargar pasillo;llave;oficina;cuatrocargar oficina;llave;-;7cargar oficina;llave;oficina;7ir este[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: 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: llaveSalidas: oeste> tomar llaveTomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».> guardarCódigo de guardado: almacén;llave;-;4> cargar almacén;martillo;-;4El código no sirve: no existe el objeto martillo.> cargar pasillo;llave;oficina;cuatroEl código no sirve: los movimientos no son un número: cuatro.> cargar oficina;llave;-;7El código no sirve: la sala oficina está cerrada con llave.> cargar oficina;llave;oficina;7Continúas donde lo dejaste.[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> ir este[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oesteEl ú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.addCommand en lugar de «ir».
ir surtomar linternair surtomar fusible[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte, sur> ir surEstá demasiado oscuro. Sin luz no encontrarás nada.> tomar linternaTomas: 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: fusibleSalidas: norte> tomar fusibleTomas: 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.
ir norteir estetomar llaveir oesteir oesteusar llaveir oestetomar notair esteir esteir oesteingresar 4719ir norte[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: 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: llaveSalidas: oeste> tomar llaveTomas: 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 oesteLa puerta hacia «oficina» está cerrada con llave.> usar llaveAbres la puerta hacia «oficina».> ir oeste[oficina] Un escritorio, una silla y un archivero lleno de carpetas.Ves: notaSalidas: este> tomar notaTomas: 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 4719La 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.
pistair norteir estetomar llavepistair oesteir oesteusar llavepista[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.Ves: linternaSalidas: norte> pistaPista: 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: llaveSalidas: oeste> tomar llaveTomas: llave. Una llavecita de latón. Su etiqueta dice «oficina».> pistaPista: la llave es para la oficina.> ir oeste[pasillo] Un pasillo largo. La luz del techo parpadea.Salidas: norte, sur, este, oeste> ir oesteLa puerta hacia «oficina» está cerrada con llave.> usar llaveAbres la puerta hacia «oficina».> pistaPista: 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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Expresiones regulares · Archivos · Arrays: push, splice y sort · Map y Set
- Ejercicios sobre el tema: Hagamos un juego