Ir al contenido
wannacode

El laboratorio cerrado 1: salas y comandos

En resumen: Un juego de escape de texto: las salas y los objetos son objetos del programa, las salidas están en un diccionario, el inventario en una lista, cada comando es una clase y un comando desconocido es una excepción.

Problema

Encerrado en el laboratorio

Te quedaste hasta tarde en el laboratorio. El sistema de seguridad cerró las puertas automáticamente. Tienes que salir antes de que lleguen los guardias por la mañana. No hay peleas, solo salas, objetos y acertijos.

El juego es de texto. El jugador escribe comandos y el juego responde:

> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: sur, este, oeste
> tomar llave
Aquí no hay llave.

La primera idea es un if enorme: si el comando es «ir», haz esto; si es «tomar», haz aquello. Crece con cada comando nuevo, y un error en una rama rompe las demás. En esta lección los comandos se convierten en clases separadas, y las salas y los objetos, en objetos dentro de colecciones.

La idea

Salas y objetos

Un objeto del juego tiene un nombre y una descripción:

class Item {
private String name;
private String description;
public Item(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}

Una sala tiene un nombre, una descripción, salidas y objetos. Las salidas son un diccionario: la dirección es la clave y la sala siguiente es el valor. Los objetos son una lista, porque cuántos hay cambia:

private static final String[] DIRECTIONS = { "norte", "sur", "este", "oeste" };
private String name;
private String description;
private Map<String, Room> exits = new HashMap<>();
private List<Item> items = new ArrayList<>();
public void addExit(String direction, Room room) {
exits.put(direction, room);
}
public Room getExit(String direction) {
return exits.get(direction);
}

get en un diccionario devuelve None si la clave no está. Así que «no hay salida por ahí» es simplemente None. Los campos se crean en __init__, y DIRECTIONS es una constante de la clase: compartida por todas las salas.

Míralo

Qué hay en la sala

Tomar un objeto lo quita de la sala y lo devuelve. Si no está, devuelve None:

public void addItem(Item item) {
items.add(item);
}
public Item takeItem(String name) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).getName().equals(name)) {
return items.remove(i);
}
}
return null;
}

La descripción reúne los nombres de los objetos y las salidas. Las direcciones van en un orden fijo (norte, sur, este, oeste), para que el texto no dependa del orden del diccionario:

public String describe() {
String text = "[" + name + "] " + description;
if (!items.isEmpty()) {
List<String> names = new ArrayList<>();
for (Item item : items) {
names.add(item.getName());
}
text += "\nVes: " + String.join(", ", names);
}
List<String> ways = new ArrayList<>();
for (String direction : DIRECTIONS) {
if (exits.containsKey(direction)) {
ways.add(direction);
}
}
return text + "\nSalidas: " + String.join(", ", ways);
}

Pruébalo

¿Salida o None?

¿Qué imprimirá el programa?

hall = Room("pasillo", "Un pasillo largo.")
store = Room("almacén", "Estantes con cajas.")
hall.add_exit("este", store)
store.add_exit("oeste", hall)
store.add_item(Item("llave", "Una llave de latón."))
print(hall.get_exit("este").name)
print(hall.get_exit("oeste") is None)
print(store.take_item("llave") is not None)
print(store.take_item("llave") is not None)
Respuesta

La idea

Cada comando es una clase

Todos los comandos siguen un mismo contrato: reciben el juego y la palabra que viene después del comando, y devuelven el texto que ve el jugador:

interface Command {
String execute(Game game, String argument);
}

El contrato es una clase abstracta (ABC) con solo un método abstracto. Cada comando la extiende y escribe su propio execute.

Tomar e ir son dos clases pequeñas. Cada una sabe solo lo suyo:

class TakeCommand implements Command {
@Override
public String execute(Game game, String argument) {
if (argument.isEmpty()) {
return "¿Qué tomo?";
}
Item item = game.getCurrent().takeItem(argument);
if (item == null) {
return "Aquí no hay " + argument + ".";
}
game.getInventory().add(item);
return "Tomas: " + item.getName() + ". " + item.getDescription();
}
}
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.";
}
game.setCurrent(next);
return next.describe();
}
}

Los comandos no imprimen, devuelven texto. Así el juego decide dónde mostrarlo, y puedes probar un comando sin mirar la pantalla.

La idea

Un diccionario de comandos

El juego guarda los comandos en un diccionario: la palabra es la clave y el objeto comando es el valor. Añadir un comando es una sola línea:

private Map<String, Room> rooms;
private Map<String, Command> commands = new HashMap<>();
private List<Item> inventory = new ArrayList<>();
private Room current;
public Game() {
rooms = buildWorld();
current = rooms.get("laboratorio");
commands.put("mirar", new LookCommand());
commands.put("tomar", new TakeCommand());
commands.put("ir", new GoCommand());
}

handle quita los espacios de los extremos, pasa la línea a minúsculas y la divide en palabras. La primera palabra es el comando y la segunda es el argumento. Si la palabra no está en el diccionario, _find lanza su propia excepción, y handle la captura y dice «No entiendo»:

public String handle(String line) {
String[] words = line.trim().toLowerCase().split(" ");
String argument = words.length > 1 ? words[1] : "";
try {
Command command = find(words[0]);
return command.execute(this, argument);
} catch (UnknownCommandException e) {
return "No entiendo «" + e.getVerb() + "».";
}
}
private Command find(String verb) throws UnknownCommandException {
Command command = commands.get(verb);
if (command == null) {
throw new UnknownCommandException(verb);
}
return command;
}
class UnknownCommandException extends Exception {
private String verb;
public UnknownCommandException(String verb) {
super("orden desconocida: " + verb);
this.verb = verb;
}
public String getVerb() {
return verb;
}
}
Más precisamente, para curiosos
  • El juego conoce solo Command. El objeto del diccionario decide qué execute se ejecuta. Es el polimorfismo del módulo de herencia, salvo que los objetos están en un diccionario y no en una lista.
  • Otra forma es un match sobre la palabra. Para cuatro comandos está bien. Para veinte, con reglas distintas cada uno, las clases separadas se leen y se cambian con más facilidad.
  • split() sin argumento divide por cualquier espacio en blanco y no da palabras vacías. Aquí un espacio es suficiente.
print("go norte".split(" "))
print("go norte".split())
Salida
['ir', '', 'norte']
['ir', 'norte']

Pruébalo

Encuentra el error

El jugador escribe «Mirar» con mayúscula, y el juego responde No entiendo «Mirar». ¿En qué línea está el error?

Elige la línea con el error.

Míralo

Un primer recorrido

El programa muestra la primera sala y después lee comando tras comando hasta que se acaba la entrada. Antes de cada comando imprime >, para que se vea qué escribió el jugador:

Game game = new Game();
System.out.println(game.getCurrent().describe());
Scanner in = new Scanner(System.in);
while (in.hasNextLine()) {
String line = in.nextLine();
System.out.println("> " + line);
System.out.println(game.handle(line));
}
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> Tomar linterna
Tomas: linterna. Funciona y la batería está casi llena.
> mirar
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Salidas: norte
> tomar linterna
Aquí no hay linterna.
> bailar
No entiendo «bailar».
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: 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: sur, este, oeste
> ir arriba
Por ahí no hay salida.
> 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.

for line in sys.stdin lee la entrada línea por línea y se detiene cuando se acaba. Cada línea llega con el salto de línea al final, y rstrip("\n") lo quita. Al principio del archivo está import sys.

Con esta entrada:

Entrada
Tomar linterna
mirar
tomar linterna
bailar
ir norte
ir este
tomar llave
ir oeste
ir arriba
ir oeste
tomar nota

La nota dice que el código del vestíbulo es 47 y el día del inventario. El calendario del almacén dice 19. Pero todavía no hay vestíbulo, y la oficina tampoco está cerrada con llave. Los acertijos llegan en la siguiente lección.

game = Game()
print(game.current.describe())
for line in sys.stdin:
line = line.rstrip("\n")
print(f"> {line}")
print(game.handle(line))
Paso a paso, en palabras
  1. El juego empieza en la sala «laboratorio». Los comandos están en un diccionario: la palabra es la clave y el objeto comando es el valor.
  2. Escribes «Tomar linterna». La línea pasa a minúsculas. «tomar» es una clave del diccionario, y el comando es TakeCommand. El objeto «linterna» pasa de la sala a la mochila.
  3. Escribes «mirar». «mirar» es una clave del diccionario, y el comando es LookCommand. Describe la sala otra vez, ahora sin el objeto que tomaste.
  4. Escribes «tomar linterna». «tomar» es una clave del diccionario, y el comando es TakeCommand. Ya no hay «linterna» en la sala, así que la respuesta es «Aquí no hay».
  5. Escribes «bailar». «bailar» no está en el diccionario: _find lanza UnknownCommandError, y handle dice «No entiendo».
  6. Escribes «ir norte». «ir» es una clave del diccionario, y el comando es GoCommand. get_exit("norte") en la sala «laboratorio» da la sala «pasillo».
  7. Escribes «ir este». «ir» es una clave del diccionario, y el comando es GoCommand. get_exit("este") en la sala «pasillo» da la sala «almacén».
  8. Escribes «tomar llave». «tomar» es una clave del diccionario, y el comando es TakeCommand. El objeto «llave» pasa de la sala a la mochila.
  9. Escribes «ir oeste». «ir» es una clave del diccionario, y el comando es GoCommand. get_exit("oeste") en la sala «almacén» da la sala «pasillo».
  10. Escribes «ir arriba». «ir» es una clave del diccionario, y el comando es GoCommand. Las salidas de la sala «pasillo» no tienen la clave «arriba»: por ahí no hay salida.
  11. Escribes «ir oeste». «ir» es una clave del diccionario, y el comando es GoCommand. get_exit("oeste") en la sala «pasillo» da la sala «oficina».
  12. Escribes «tomar nota». «tomar» es una clave del diccionario, y el comando es TakeCommand. El objeto «nota» pasa de la sala a la mochila.
  13. La entrada se acabó, y el bucle se detiene. La mochila: linterna, llave, nota.

Ejercicio

Más comandos y salas

Resuelve las tareas en tu editor, con las clases de la lección. Añade lo nuevo desde fuera, con game.add_command y game.get_room, sin tocar Game.

1. Inventario. Un comando «inventario» dice qué hay en la mochila o que está vacía.

Entrada
inventario
tomar linterna
ir norte
ir este
tomar llave
inventario
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> inventario
Tu mochila está vacía.
> tomar linterna
Tomas: linterna. Funciona y la batería está casi llena.
> ir norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: 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».
> inventario
En la mochila: linterna, llave

2. Una sala nueva. Una sala de servidores al norte del almacén, con un cable de red dentro. Su descripción es «Racks con luces intermitentes. Los ventiladores zumban.»

Entrada
ir norte
ir este
ir norte
tomar cable
ir sur
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: 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: norte, oeste
> ir norte
[sala de servidores] Racks con luces intermitentes. Los ventiladores zumban.
Ves: cable
Salidas: sur
> tomar cable
Tomas: cable. Un cable de red largo.
> ir sur
[almacén] Estantes con cajas hasta el techo. Un calendario en la pared dice: «Inventario el 19 de octubre».
Ves: llave
Salidas: norte, oeste

3. Sinónimos. Haz que «agarrar» signifique «tomar», «caminar» signifique «ir» y «ver» signifique «mirar». Pista: el mismo comando puede ser el valor de varias claves.

Entrada
agarrar linterna
caminar norte
ver
Salida
[laboratorio] Las mesas están llenas de matraces. La puerta a tus espaldas se bloqueó sola.
Ves: linterna
Salidas: norte
> agarrar linterna
Tomas: linterna. Funciona y la batería está casi llena.
> caminar norte
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: sur, este, oeste
> ver
[pasillo] Un pasillo largo. La luz del techo parpadea.
Salidas: sur, este, oeste

Más tareas

  • Soltar. Un comando «soltar llave» devuelve el objeto de la mochila a la sala.
  • Ayuda. Un comando «ayuda» enumera todas las palabras del diccionario de comandos.
  • Direcciones cortas. Haz que «n», «s», «e», «o» signifiquen norte, sur, este, oeste.

Test

Ponte a prueba

1. El jugador escribe «IR norte» (con dos espacios). ¿Qué recibe handle después de strip, de pasar a minúsculas y de dividir por un espacio?
2. ¿Por qué cada comando es una clase aparte y no una rama de un if/else grande?
3. ¿Qué devuelve get_exit para una dirección que no está en el diccionario?
4. Un comando desconocido lanza UnknownCommandError. ¿Quién la captura?

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