Una pila de pantallas
En resumen: Una pila de pantallas mantiene las pantallas abiertas una sobre otra: la entrada va a la de arriba, push abre una nueva, pop la cierra, replace la cambia, y las transparentes se dibujan sobre las que tienen debajo.
Problema
Una variable para todas las pantallas
El juego tiene una pantalla de título, una aldea, el mapa, un combate, un inventario que se abre sobre el mapa y un menú que se abre sobre el combate. Cada pantalla lee los comandos a su manera: en el mapa a es un paso a la izquierda, en el combate es un golpe, en el inventario no es nada.
El primer intento que casi todos hacen: una variable con el nombre de la pantalla y un if sobre ella. Una vez al manejar el comando y otra vez al dibujar:
import { createInterface } from "node:readline";
const rl = createInterface({ input: process.stdin });let screen = "title";let wolf = 2;for await (const line of rl) { const command = line.trim(); if (screen === "title") { if (command === "n") screen = "map"; } else if (screen === "map") { if (command === "i") screen = "inventory"; if (command === "f") screen = "battle"; } else if (screen === "battle") { if (command === "i") screen = "inventory"; if (command === "a") { wolf--; if (wolf === 0) screen = "map"; } } else if (screen === "inventory") { if (command === "x") screen = "map"; } process.stdout.write(command + " → "); if (screen === "title") console.log("Las crónicas de Arcanum"); else if (screen === "map") console.log(`Mapa, lobo: ${wolf}`); else if (screen === "battle") console.log(`Combate, lobo: ${wolf}`); else if (screen === "inventory") console.log("Inventario: Poción x2");}n → Mapa, lobo: 2i → Inventario: Poción x2x → Mapa, lobo: 2f → Combate, lobo: 2i → Inventario: Poción x2x → Mapa, lobo: 2a → Mapa, lobo: 2a → Mapa, lobo: 2Los comandos llegan uno por línea: n juego nuevo, i inventario, x cerrar, f luchar contra el lobo, a golpe.
nixfixaaMira la sexta línea. El inventario se abrió desde el combate, y x lo cierra hacia el mapa. El combate ya no está, el lobo sigue vivo con 2 de vida, y los dos golpes siguientes caen en el mapa, donde a no significa nada. La variable recuerda dónde estás, pero no de dónde vienes.
El segundo problema aparece cuando el juego crece. Cada pantalla nueva es una rama nueva en cada if: en el manejo, en el dibujo y en cada pantalla que la abre. Un menú sobre el combate y sobre el mapa son dos ramas más en dos lugares. Si olvidas una, tienes una pantalla que recibe comandos pero nunca se dibuja.
La idea
Una pantalla decide sobre sí misma
Da la vuelta a la pregunta. En vez de que el bucle pregunte «¿en qué pantalla estoy?» y conozca todas las pantallas, cada pantalla es un objeto con los mismos cuatro métodos:
handle: qué significa el comando aquí;update: qué pasa en cada turno mientras la pantalla está arriba (en el combate, el lobo muerde);draw: qué dibuja la pantalla;isTransparent: si la pantalla de abajo se ve a través.
// JavaScript no tiene clases abstractas: aquí handle y draw no hacen nada,// y cada pantalla los escribe de nuevo.class Screen { constructor(game, name) { this.game = game; this.name = name; }
isTransparent() { return false; }
handle(command) {}
update() {}
draw(buffer) {}}handle y draw vacíos. Cada pantalla escribe los suyos. Si lo olvidas, la pantalla simplemente no hace nada, sin ningún error.Aquí están el combate y el inventario. El combate solo sabe de sí mismo: i abre el inventario encima, a golpea, y cuando el lobo cae, el combate se quita a sí mismo. El inventario no sabe desde dónde se abrió. Solo dice «quítame»:
class BattleScreen extends Screen { constructor(game) { super(game, "Combate"); }
handle(command) { if (command === "i") this.game.stack.push(new InventoryScreen(this.game)); if (command === "a") { this.game.wolf--; if (this.game.wolf === 0) this.game.stack.pop(); } }
// En cada turno del combate el lobo muerde. update() { if (this.game.wolf > 0) this.game.life--; }
draw(buffer) { buffer.box(0, 0, TextBuffer.W, TextBuffer.H); buffer.write(2, 1, `Lobo: ${this.game.wolf}`); buffer.write(2, 2, `Vida: ${this.game.life}`); buffer.write(2, 3, "a: golpe i: mochila"); }}class InventoryScreen extends Screen { constructor(game) { super(game, "Inventario"); }
handle(command) { if (command === "x") this.game.stack.pop(); }
draw(buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Poción x2"); }}Las pantallas dibujan en un TextBuffer: cinco filas de 24 caracteres. write pone texto en un lugar dado, box dibuja un marco. El búfer completo está en el programa de abajo, y cómo funciona el dibujo en una terminal real está en la lección sobre el dibujo.
Una pantalla nueva es una clase nueva y una línea en la pantalla que la abre. No crece ningún if sobre nombres de pantalla.
Más precisamente, para curiosos
Así se ve en el juego. La región screen del canon:
// Cada pantalla es un objeto con la misma interfaz. El juego no pregunta «en qué pantalla estoy»; manda el comando a// la pantalla que está arriba de la pila.class Screen { constructor(game) { this.game = game; }
// Una pantalla transparente (menú, inventario, diálogo) deja ver la pantalla de debajo. isTransparent() { return false; }
handle(command) {}
update() {}
draw(buffer) {}
// La pantalla de debajo de la que se quita recibe su respuesta (el objetivo elegido, «sí» o «no», la ranura elegida). onResult(result) {}}El método onResult es para las pantallas que devuelven una respuesta (el objetivo elegido, «sí» o «no»). Es el tema de la siguiente lección.
La idea
La pila: de dónde vienes
Las pantallas se apilan una sobre otra: el inventario sobre el combate, el combate sobre el mapa. Cuando cierras la de arriba, quieres estar exactamente donde estabas. Eso es una pila: el último que entra es el primero que sale. La pila de pantallas tiene tres operaciones:
- push: abrir una pantalla nueva encima (el mapa abre el combate, el combate abre el inventario);
- pop: cerrar la de arriba y volver a la de debajo;
- replace: cambiar la de arriba por una nueva, sin camino de vuelta (la pantalla de título se convierte en el mapa, la creación del héroe se convierte en la aldea).
class ScreenStack { constructor() { this.screens = []; }
push(screen) { this.screens.push(screen); }
pop() { this.screens.pop(); }
replace(screen) { this.pop(); this.push(screen); }
top() { return this.screens[this.screens.length - 1]; }
names() { return `[${this.screens.map((s) => s.name).join(", ")}]`; }
draw(buffer) { this.top().draw(buffer); }}El bucle del juego da el comando solo a la cima. No al mapa, no al combate que hay bajo el inventario. Solo la cima sabe qué significa el comando en este momento. Los mismos comandos, ahora con la pila. show imprime la pila y el fotograma después de cada comando:
function show(game, buffer) { console.log(game.stack.names()); buffer.clear(); game.stack.draw(buffer); buffer.print();}
const game = new Game();const buffer = new TextBuffer();game.stack.push(new TitleScreen(game));show(game, buffer);const rl = createInterface({ input: process.stdin });for await (const line of rl) { const command = line.trim(); console.log(`> ${command}`); const top = game.stack.top(); top.handle(command); top.update(); show(game, buffer);}[Título]
Las crónicas de Arcanum
n: juego nuevo
> n[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> i[Mapa, Inventario]
+---------+ |Poción x2| +---------+
> x[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> f[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 10 || a: golpe i: mochila |+----------------------+> i[Mapa, Combate, Inventario]
+---------+ |Poción x2| +---------+
> x[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 9 || a: golpe i: mochila |+----------------------+> a[Mapa, Combate]+----------------------+| Lobo: 1 || Vida: 8 || a: golpe i: mochila |+----------------------+> a[Mapa]#########################..@......~~~..........##.........~~~..........######################### Vida: 8El inventario abierto desde el combate ahora se cierra de vuelta al combate: [Mapa, Combate]. El lobo cae con el segundo golpe, el combate se quita a sí mismo, y debajo está el mapa, ahora sin la w. Ninguna pantalla recuerda de dónde vino. La pila sí.
Pero mira los fotogramas con el inventario. El mapa ha desaparecido, y el marco cuelga en el vacío. El draw de la pila dibuja solo la cima. De eso trata lo siguiente.
Más precisamente, para curiosos
Solo se actualiza la cima. El bucle llama a update en la cima. Mientras el inventario está abierto sobre el combate, el combate espera y el lobo no muerde: el juego está en pausa bajo el inventario. Es una decisión, no una ley. En un juego en tiempo real el mundo bajo el menú puede seguir, y entonces la pila también actualiza las pantallas por debajo de la cima que dicen que no se detienen.
pop sobre una pila vacía. Si la última pantalla se quita a sí misma, la pila queda vacía y el siguiente pop no tiene nada que quitar. En JavaScript, pop() sobre un arreglo vacío devuelve undefined sin ningún error, y el programa falla más tarde, en top().handle(...), lejos de la causa real. En el juego una pila vacía significa el final: el bucle corre hasta que la pila está vacía, y «Salir» en la pantalla de título vacía la pila.
Pruébalo
Predice la salida
La pila está vacía. ¿Qué hay en ella al final?
const game = new Game();const stack = game.stack;stack.push(new TitleScreen(game));stack.replace(new MapScreen(game));stack.push(new BattleScreen(game));stack.push(new InventoryScreen(game));stack.pop();stack.pop();stack.push(new InventoryScreen(game));console.log(stack.names());La idea
Pantallas transparentes
El inventario es un marco pequeño sobre el mapa. Para que el mapa se vea alrededor, hay que dibujar primero el mapa y después el inventario, encima. Como un pintor que pinta primero el fondo y luego las figuras de delante.
Pero no desde la pantalla de abajo del todo. Bajo el combate está el mapa, y el combate lo cubre entero: dibujarlo sería trabajo perdido. Así que el dibujo empieza en la pantalla opaca más alta. Baja desde la cima mientras las pantallas sean transparentes. La primera que no lo sea es el comienzo. Desde ahí, dibuja hacia arriba:
draw(buffer) { let first = this.screens.length - 1; while (first > 0 && this.screens[first].isTransparent()) first--; for (let i = first; i < this.screens.length; i++) { this.screens[i].draw(buffer); }}El inventario dice que es transparente:
class InventoryScreen extends Screen { constructor(game) { super(game, "Inventario"); }
isTransparent() { return true; }
handle(command) { if (command === "x") this.game.stack.pop(); }
draw(buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Poción x2"); }}Solo estos dos cambios. El mismo bucle, los mismos comandos:
[Título]
Las crónicas de Arcanum
n: juego nuevo
> n[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> i[Mapa, Inventario]#########################..@......~~+---------+##.......w.~~|Poción x2|#############+---------+# Vida: 10> x[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> f[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 10 || a: golpe i: mochila |+----------------------+> i[Mapa, Combate, Inventario]+----------------------+| Lobo: 2 +---------+|| Vida: 9 |Poción x2||| a: golpe +---------+|+----------------------+> x[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 9 || a: golpe i: mochila |+----------------------+> a[Mapa, Combate]+----------------------+| Lobo: 1 || Vida: 8 || a: golpe i: mochila |+----------------------+> a[Mapa]#########################..@......~~~..........##.........~~~..........######################### Vida: 8[Mapa, Inventario]: el mapa, con el marco encima, y el lobo se sigue viendo debajo. [Mapa, Combate, Inventario]: el dibujo empieza en el combate, porque no es transparente. El mapa que hay debajo ni siquiera se dibuja.
first > 0 se detiene en la pantalla de abajo del todo: esa siempre se dibuja, aunque sea transparente, porque no hay nada debajo.
Más precisamente, para curiosos
El método draw de la región screen-stack del canon:
// El dibujo empieza en la pantalla opaca de más arriba y sube desde ahí.draw(buffer) { let first = this.screens.length - 1; while (first > 0 && this.screens[first].isTransparent()) first--; for (let i = first; i < this.screens.length; i++) { if (i >= 0) this.screens[i].draw(buffer); }}La comprobación i >= 0 es para una pila vacía: entonces first es −1 y no hay nada que dibujar.
¿Por qué no simplemente desde cero? Si cada pantalla opaca lo cubre todo, el resultado es el mismo, pero cada fotograma dibuja todas las pantallas de abajo: mapa, combate, elección de objetivo, diálogo. Y hay algo peor: una pantalla opaca que no escribe todas las celdas deja ver trozos del mapa. «No soy transparente» es una promesa que dice «lo cubro todo», y la pila cuenta con ella.
Míralo
Turno a turno
El mismo programa con el inventario transparente. A la izquierda está la pila: cada placa es una pantalla con lo que dibuja por sí sola. El comando va a la placa de arriba. Las placas desvanecidas están bajo la pantalla opaca más alta y no se dibujan. Bajo la pila está el fotograma que imprime el programa.
class ScreenStack { constructor() { this.screens = []; }
push(screen) { this.screens.push(screen); }
pop() { this.screens.pop(); }
replace(screen) { this.pop(); this.push(screen); }
top() { return this.screens[this.screens.length - 1]; }
names() { return `[${this.screens.map((s) => s.name).join(", ")}]`; }
draw(buffer) { let first = this.screens.length - 1; while (first > 0 && this.screens[first].isTransparent()) first--; for (let i = first; i < this.screens.length; i++) { this.screens[i].draw(buffer); } }}
for await (const line of rl) { const command = line.trim(); console.log(`> ${command}`); const top = game.stack.top(); top.handle(command); top.update(); show(game, buffer);}Paso a paso, en palabras
- El juego empieza con una pantalla en la pila: la pantalla de título. Está en la cima, así que recibe la entrada.
- n va a la pantalla de título. Esta pone el mapa en su propio lugar (replace): la pantalla de título no va a volver.
- i va al mapa, que pone el inventario encima (push). El inventario es transparente: primero se dibuja el mapa y luego el inventario sobre él.
- x va al inventario: se quita a sí mismo (pop). Debajo está el mapa, exactamente como lo dejaste.
- f: el mapa pone el combate encima (push). El combate no es transparente, así que el mapa que hay debajo ni siquiera se dibuja.
- Ahora i va al combate, no al mapa. El combate abre el inventario sobre sí mismo. El dibujo empieza en el combate, la pantalla opaca más alta. El lobo mordió: vida 9.
- x cierra el inventario y debajo está el combate: vuelves de donde viniste. La variable única del principio de la lección te llevó aquí al mapa.
- a va al combate: el lobo recibe el golpe, y la pila no cambia. Se ejecuta el update del combate: el lobo muerde de vuelta, vida 8.
- El lobo cae. El combate se quita a sí mismo (pop), y el mapa vuelve a estar en la cima, ya sin el lobo.
- La entrada va a la cima. El dibujo sube desde la pantalla opaca más alta. Las pantallas que devuelven una respuesta y los cambios que esperan al final del turno son la lección siguiente.
Pruébalo
¿Qué falta?
El dibujo de la pila. Mientras la pantalla en first sea transparente, baja:
draw(buffer) {
let first = this.screens.length - 1;
while (first > 0 && ) first--;
for (let i = first; i < this.screens.length; i++) {
this.screens[i].draw(buffer);
}
}La pregunta es si la pantalla en la posición first es transparente: this.screens[first].isTransparent(). Si lo es, también hay que dibujar la de debajo.
Pruébalo
Encuentra el error
Un compañero reescribió la pila. El programa con los comandos n, i, x imprime:
[Título]
Las crónicas de Arcanum
n: juego nuevo
> n[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> i[Mapa, Inventario]#########################..@......~~+---------+##.......w.~~|Poción x2|#############+---------+# Vida: 10> x[Mapa, Inventario]#########################..@......~~+---------+##.......w.~~|Poción x2|#############+---------+# Vida: 10El inventario no se cierra: después de x la pila sigue siendo [Mapa, Inventario]. ¿En qué línea está el error?
Elige la línea con el error.
La cima de la pila es el último elemento, no el primero. Con this.screens[0] el comando va al mapa que está del todo abajo. El mapa no sabe qué significa x, y el inventario nunca lo oye. La lista crece por el final, así que push, pop y top trabajan todos con su final.
La idea
La pila en el juego real
La región screen-stack del canon. Los métodos son los mismos, con algunas diferencias:
// La pila no cambia durante el procesamiento: push, pop y replace solo anotan el cambio, y// apply() lo aplica después del turno. Si no, una pantalla que se quita de la pila seguiría funcionando.class ScreenStack { constructor() { this.screens = []; this.changes = []; }
push(screen) { this.changes.push({ kind: 'push', screen: screen, result: null }); }
pop(result) { this.changes.push({ kind: 'pop', screen: null, result: result }); }
replace(screen) { this.changes.push({ kind: 'replace', screen: screen, result: null }); }
// Vacía toda la pila (cargar, fin del juego); con null la pila queda vacía. reset(screen) { this.changes.push({ kind: 'reset', screen: screen, result: null }); }
top() { return this.screens.length === 0 ? null : this.screens[this.screens.length - 1]; }
isEmpty() { return this.screens.length === 0 && this.changes.length === 0; }
contains(kindName) { for (const screen of this.screens) { if (screen.kindName === kindName) return true; } return false; }
apply() { let index = 0; while (index < this.changes.length) { const change = this.changes[index]; index++; if (change.kind === 'push') { this.screens.push(change.screen); } else if (change.kind === 'pop') { this.screens.pop(); const below = this.top(); if (below !== null) below.onResult(change.result); } else if (change.kind === 'replace') { this.screens.pop(); this.screens.push(change.screen); } else { this.screens = []; if (change.screen !== null) this.screens.push(change.screen); } } this.changes = []; }
// El dibujo empieza en la pantalla opaca de más arriba y sube desde ahí. draw(buffer) { let first = this.screens.length - 1; while (first > 0 && this.screens[first].isTransparent()) first--; for (let i = first; i < this.screens.length; i++) { if (i >= 0) this.screens[i].draw(buffer); } }}- push, pop y replace no tocan la lista de inmediato. Anotan el cambio en
changes, yapply()los ejecuta después del turno. En nuestro programa el combate se quita a sí mismo enhandle, y justo después el bucle llama aupdateen ese mismo combate, que ya no está en la pila. Aquí funciona porque un lobo muerto no muerde. En un juego más grande es un error que tarda mucho en encontrarse. Por qué y cómo se aplazan los cambios es la siguiente lección. - pop lleva una respuesta: la pantalla de debajo la recibe en
onResult. Así la pantalla de elección de objetivo devuelve el objetivo al combate. Eso también está en la siguiente lección. - reset vacía toda la pila (cargar una partida, el final del juego), y contains pregunta si una pantalla de cierto tipo ya está en la pila (por ejemplo, para que no se abra una segunda pantalla de subida de nivel).
Más precisamente
La misma idea en otros lugares
La navegación en las apps móviles es una pila de pantallas. En Android y iOS abres una pantalla encima (push), y el botón Atrás la quita (pop). Entrar después del inicio de sesión suele ser un replace: Atrás no debe llevar otra vez al formulario de acceso.
Un diálogo modal en el navegador es una pantalla sobre la página. Mientras está abierto, solo él recibe clics y teclas, y la página de debajo se ve pero no reacciona. Igual que el inventario sobre el mapa.
La pila de llamadas es la misma idea en el lenguaje mismo. Un método llama a otro (push), ese devuelve (pop), y el que llamó continúa exactamente donde se quedó. El combate continúa después del inventario de la misma manera.
Cuando una pila no basta. Una pila dice que hay exactamente una pantalla arriba y que recibe todo. A veces eso no es verdad:
- dos pantallas lado a lado que trabajan juntas: un mapa y un minimapa, un combate y el registro del combate, un juego de dos jugadores en pantalla dividida. Entonces la pantalla es un árbol de paneles, y la entrada va al panel con el foco;
- cosas que siempre están arriba, sea cual sea la pantalla: una barra de vida, un aviso «Partida guardada». Son una capa aparte, que se dibuja después de la pila;
- ventanas que se cierran en cualquier orden (un juego de estrategia con cinco ventanas abiertas). Eso ya no es una pila sino una lista con foco.
Ejercicio
Un menú sobre todo
Resuélvelo en tu propio editor. El programa completo con pantallas transparentes está debajo de la tarea.
Menú. Añade un MenuScreen con el nombre «Menú». m lo abre desde el mapa, desde el combate y desde el inventario. El menú es transparente, y x lo cierra. Dibuja un marco box(14, 2, 10, 3) con x: atrás dentro, en (15, 3).
nfimxxaa[Título]
Las crónicas de Arcanum
n: juego nuevo
> n[Mapa]#########################..@......~~~..........##.......w.~~~..........######################### Vida: 10> f[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 10 || a: golpe i: mochila |+----------------------+> i[Mapa, Combate, Inventario]+----------------------+| Lobo: 2 +---------+|| Vida: 9 |Poción x2||| a: golpe +---------+|+----------------------+> m[Mapa, Combate, Inventario, Menú]+----------------------+| Lobo: 2 +---------+|| Vida: 9 |P+--------+| a: golpe +-|x: atrás|+-------------+--------+> x[Mapa, Combate, Inventario]+----------------------+| Lobo: 2 +---------+|| Vida: 9 |Poción x2||| a: golpe +---------+|+----------------------+> x[Mapa, Combate]+----------------------+| Lobo: 2 || Vida: 9 || a: golpe i: mochila |+----------------------+> a[Mapa, Combate]+----------------------+| Lobo: 1 || Vida: 8 || a: golpe i: mochila |+----------------------+> a[Mapa]#########################..@......~~~..........##.........~~~..........######################### Vida: 8Mira el fotograma con el menú: se dibujan el combate, el inventario y el menú, en ese orden. El menú cubre una esquina del inventario, porque está más arriba en la pila.
Más precisamente, para curiosos
El programa completo con pantallas transparentes:
import { createInterface } from "node:readline";
class TextBuffer { static W = 24; static H = 5;
constructor() { this.rows = []; }
clear() { this.rows = []; for (let y = 0; y < TextBuffer.H; y++) this.rows.push(" ".repeat(TextBuffer.W)); }
write(x, y, text) { const row = this.rows[y]; this.rows[y] = row.substring(0, x) + text + row.substring(x + text.length); }
box(x, y, w, h) { const edge = `+${"-".repeat(w - 2)}+`; this.write(x, y, edge); for (let i = 1; i < h - 1; i++) this.write(x, y + i, `|${" ".repeat(w - 2)}|`); this.write(x, y + h - 1, edge); }
print() { for (const row of this.rows) console.log(row.trimEnd()); }}
class Game { constructor() { this.stack = new ScreenStack(); this.life = 10; this.wolf = 2; }}
// JavaScript no tiene clases abstractas: aquí handle y draw no hacen nada,// y cada pantalla los escribe de nuevo.class Screen { constructor(game, name) { this.game = game; this.name = name; }
isTransparent() { return false; }
handle(command) {}
update() {}
draw(buffer) {}}
class ScreenStack { constructor() { this.screens = []; }
push(screen) { this.screens.push(screen); }
pop() { this.screens.pop(); }
replace(screen) { this.pop(); this.push(screen); }
top() { return this.screens[this.screens.length - 1]; }
names() { return `[${this.screens.map((s) => s.name).join(", ")}]`; }
draw(buffer) { let first = this.screens.length - 1; while (first > 0 && this.screens[first].isTransparent()) first--; for (let i = first; i < this.screens.length; i++) { this.screens[i].draw(buffer); } }}
class TitleScreen extends Screen { constructor(game) { super(game, "Título"); }
handle(command) { if (command === "n") this.game.stack.replace(new MapScreen(this.game)); }
draw(buffer) { buffer.write(1, 1, "Las crónicas de Arcanum"); buffer.write(1, 3, "n: juego nuevo"); }}
class MapScreen extends Screen { static ROWS = [ "########################", "#..@......~~~..........#", "#.........~~~..........#", "########################", ];
constructor(game) { super(game, "Mapa"); }
handle(command) { if (command === "i") this.game.stack.push(new InventoryScreen(this.game)); if (command === "f" && this.game.wolf > 0) this.game.stack.push(new BattleScreen(this.game)); }
draw(buffer) { MapScreen.ROWS.forEach((row, y) => buffer.write(0, y, row)); if (this.game.wolf > 0) buffer.write(8, 2, "w"); buffer.write(1, 4, `Vida: ${this.game.life}`); }}
class BattleScreen extends Screen { constructor(game) { super(game, "Combate"); }
handle(command) { if (command === "i") this.game.stack.push(new InventoryScreen(this.game)); if (command === "a") { this.game.wolf--; if (this.game.wolf === 0) this.game.stack.pop(); } }
// En cada turno del combate el lobo muerde. update() { if (this.game.wolf > 0) this.game.life--; }
draw(buffer) { buffer.box(0, 0, TextBuffer.W, TextBuffer.H); buffer.write(2, 1, `Lobo: ${this.game.wolf}`); buffer.write(2, 2, `Vida: ${this.game.life}`); buffer.write(2, 3, "a: golpe i: mochila"); }}
class InventoryScreen extends Screen { constructor(game) { super(game, "Inventario"); }
isTransparent() { return true; }
handle(command) { if (command === "x") this.game.stack.pop(); }
draw(buffer) { buffer.box(12, 1, 11, 3); buffer.write(13, 2, "Poción x2"); }}
function show(game, buffer) { console.log(game.stack.names()); buffer.clear(); game.stack.draw(buffer); buffer.print();}
const game = new Game();const buffer = new TextBuffer();game.stack.push(new TitleScreen(game));show(game, buffer);const rl = createInterface({ input: process.stdin });for await (const line of rl) { const command = line.trim(); console.log(`> ${command}`); const top = game.stack.top(); top.handle(command); top.update(); show(game, buffer);}Más tareas
- Creación del héroe. Entre la pantalla de título y el mapa, añade una pantalla que pida un nombre. Las dos transiciones usan replace: desde el héroe no hay camino de vuelta.
- Pausa.
pabre una pantalla opaca «Pausa» desde cualquier pantalla. Comprueba que el lobo no muerde mientras el juego está en pausa. - Guarda de la pila. Haz que
popse niegue cuando solo queda una pantalla en la pila, e imprime el motivo.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Polimorfismo · Arrays: push, splice y sort · Pila, cola y lista enlazada · Funciones flecha
- Ejercicios sobre el tema: Las crónicas de Arcanum