Escribir un motor de aventuras de texto multijugador en Node.js: crear el cliente terminal (Parte 3)

 

 

 


Índice
  1. Revisando el diseño original
  2. Las herramientas que necesitaremos
  3. Arquitectura del módulo
    1. Controladores de pantalla
    2. Controladores de widgets
    3. Tipos de widgets
    4. Múltiples pantallas
  4. Ejemplos de código
    1. Uso de archivos de configuración para generar automáticamente la interfaz de usuario
    2. Comunicación entre la interfaz de usuario y la lógica empresarial
    3. Comunicación con el motor del juego
  5. Ultimas palabras

Esta tercera parte de la serie se centrará en agregar un cliente basado en texto para el motor del juego que se creó en la parte 2 . Fernando Doglio explica el diseño arquitectónico básico, la selección de herramientas y los aspectos destacados del código mostrándole cómo crear una interfaz de usuario basada en texto con la ayuda de Node.js.

 

Primero les mostré cómo definir un proyecto como este y les brindé los conceptos básicos de la arquitectura y la mecánica detrás del motor del juego. Luego, les mostré la implementación básica del motor : una API REST básica que le permite atravesar un mundo definido por JSON.

Hoy les mostraré cómo crear un cliente de texto de la vieja escuela para nuestra API usando nada más que Node.js.

Otras partes de esta serie

  • Parte 1 : La Introducción
  • Parte 2 : Diseño del servidor Game Engine
  • Parte 4 : Agregar chat a nuestro juego

Revisando el diseño original

Cuando propuse por primera vez una estructura alámbrica básica para la interfaz de usuario , propuse cuatro secciones en la pantalla:

( Vista previa grande )

Aunque en teoría eso parece correcto, me perdí el hecho de que cambiar entre enviar comandos del juego y mensajes de texto sería una molestia, por lo que en lugar de que nuestros jugadores cambien manualmente, haremos que nuestro analizador de comandos se asegure de que sea capaz de discernir si Estamos intentando comunicarnos con el juego o con nuestros amigos.

 

Entonces, en lugar de tener cuatro secciones en nuestra pantalla, ahora tendremos tres:

( Vista previa grande )

Esa es una captura de pantalla real del cliente final del juego. Puedes ver la pantalla del juego a la izquierda y el chat a la derecha, con un único cuadro de entrada común en la parte inferior. El módulo que estamos usando nos permite personalizar colores y algunos efectos básicos. Podrás clonar este código de Github y hacer lo que quieras con la apariencia.

Sin embargo, una advertencia: aunque la captura de pantalla anterior muestra el chat funcionando como parte de la aplicación, mantendremos este artículo enfocado en configurar el proyecto y definir un marco donde podamos crear una aplicación dinámica basada en una interfaz de usuario de texto. Nos centraremos en agregar soporte por chat en el próximo y último capítulo de esta serie.

Las herramientas que necesitaremos

Aunque existen muchas bibliotecas que nos permiten crear herramientas CLI con Node.js, agregar una interfaz de usuario basada en texto es una bestia completamente diferente de domar. En particular, solo pude encontrar una biblioteca (muy completa, claro está) que me permitía hacer exactamente lo que quería: Blessed .

Esta biblioteca es muy poderosa y proporciona muchas funciones que no usaremos para este proyecto (como proyectar sombras, arrastrar y soltar, y otras). Básicamente, vuelve a implementar toda la biblioteca ncurses (una biblioteca C que permite a los desarrolladores crear UI basadas en texto) que no tiene enlaces Node.js, y lo hace directamente en JavaScript; entonces, si fuera necesario, podríamos verificar su código interno (algo que no recomendaría a menos que sea absolutamente necesario).

Aunque la documentación de Blessed es bastante extensa, consiste principalmente en detalles individuales sobre cada método proporcionado (en lugar de tener tutoriales que explican cómo usar esos métodos juntos) y carece de ejemplos en todas partes, por lo que puede ser difícil profundizar en ella. si tiene que entender cómo funciona un método en particular. Dicho esto, una vez que lo entiendes, todo funciona de la misma manera, lo cual es una gran ventaja ya que no todas las bibliotecas o incluso todos los idiomas (te estoy mirando, PHP) tienen una sintaxis consistente.

Pero dejando la documentación a un lado; La gran ventaja de esta biblioteca es que funciona según las opciones JSON. Por ejemplo, si quisieras dibujar un cuadro en la esquina superior derecha de la pantalla, harías algo como esto:

var box = blessed.box({ top: ‘0', right: '0', width: '50%', height: '50%', content: 'Hello {bold}world{/bold}!', tags: true, border: { type: 'line' }, style: { fg: 'white', bg: 'magenta', border: { fg: '#f0f0f0' }, hover: { bg: 'green' } }});

Como puedes imaginar, allí también se definen otros aspectos de la caja (como su tamaño), que perfectamente puede ser dinámica en función del tamaño, tipo de borde y colores del terminal, incluso para eventos de desplazamiento. Si ha realizado desarrollo front-end en algún momento, encontrará mucha superposición entre los dos.

 

Lo que intento señalar aquí es que todo lo relacionado con la representación del cuadro se configura a través del objeto JSON pasado al boxmétodo. Eso, para mí, es perfecto porque puedo extraer fácilmente ese contenido en un archivo de configuración y crear una lógica de negocios capaz de leerlo y decidir qué elementos dibujar en la pantalla. Lo más importante es que nos ayudará a vislumbrar cómo quedarán una vez dibujados.

Esta será la base para todo el aspecto de la interfaz de usuario de este módulo (¡ más sobre eso en un segundo! ).

Arquitectura del módulo

La arquitectura principal de este módulo se basa completamente en los widgets de la interfaz de usuario que mostraremos. Un grupo de estos widgets se considera una pantalla y todas estas pantallas se definen en un único archivo JSON (que puede encontrar dentro de la /configcarpeta).

Este archivo tiene más de 250 líneas, por lo que mostrarlo aquí no tiene sentido. Puede consultar el archivo completo en línea, pero un pequeño fragmento se parece a este:

"screens": { "main-options": { "file": "./main-options.js", "elements": { "username-request": { "type": "input-prompt", "params": { "position": { "top": "0%", "left": "0%", "width": "100%", "height": "25%" }, "content": "Input your username: ", "inputOnFocus": true, "border": { "type": "line" }, "style": { "fg": "white", "bg": "blue", "border": { "fg": "#f0f0f0" }, "hover": { "bg": "green" } } } }, "options": { "type": "window", "params": { "position": { "top": "25%", "left": "0%", "width": "100%", "height": "50%" }, "content": "Please select an option: n1. Join an existing game.n2. Create a new game", "border": { "type": "line" }, "style": { //... } } }, "input": { "type": "input", "handlerPath": "../lib/main-options-handler", //... } } }

El elemento "pantallas" contendrá la lista de pantallas dentro de la aplicación. Cada pantalla contiene una lista de widgets (que cubriré en un momento) y cada widget tiene su definición específica de bendiciones y archivos de controlador relacionados (cuando corresponda).

 

Puede ver cómo cada elemento "params" (dentro de un widget en particular) representa el conjunto real de parámetros esperados por los métodos que vimos anteriormente. El resto de las claves definidas allí ayudan a proporcionar contexto sobre qué tipo de widgets representar y su comportamiento.

Algunos puntos de interés:

Controladores de pantalla

Cada elemento de la pantalla tiene una propiedad de archivo que hace referencia al código asociado a esa pantalla. Este código no es más que un objeto que debe tener un initmétodo (la lógica de inicialización para esa pantalla en particular tiene lugar dentro de él). En particular, el motor de interfaz de usuario principal llamará a ese initmétodo de cada pantalla, que a su vez, debería ser responsable de inicializar cualquier lógica que pueda necesitar (es decir, configurar los eventos de los cuadros de entrada).

El siguiente es el código para la pantalla principal, donde la aplicación solicita al jugador que seleccione una opción para iniciar un juego nuevo o unirse a uno existente:

const logger = require("../utils/logger")module.exports = { init: function(elements, UI) { this.elements = elements this.UI = UI this.id = "main-options" this.setInput() }, moveToIDRequest: function(handler) { return this.UI.loadScreen('id-requests', (err, ) = { }) }, createNewGame: function(handler) { handler.createNewGame(this.UI.gamestate.APIKEY, (err, gameData) = { this.UI.gamestate.gameID = gameData._id handler.joinGame(this.UI.gamestate, (err) = { return this.UI.loadScreen('main-ui', { flashmessage: "You've joined game " + this.UI.gamestate.gameID + " successfully" }, (err, ) = { }) }) }) }, setInput: function() { let handler = require(this.elements["input"].meta.handlerPath) let input = this.elements["input"].obj let usernameRequest = this.elements['username-request'].obj let usernameRequestMeta = this.elements['username-request'].meta let question = usernameRequestMeta.params.content.trim() usernameRequest.setValue(question) this.UI.renderScreen() let validOptions = { 1: this.moveToIDRequest.bind(this), 2: this.createNewGame.bind(this) } usernameRequest.on('submit', (username) = { logger.info("Username:" +username) logger.info("Playername: " + username.replace(question, '')) this.UI.gamestate.playername = username.replace(question, '') input.focus() input.on('submit', (data) = { let command = input.getValue() if(!validOptions[+command]) { this.UI.setUpAlert("Invalid option: " + command) return this.UI.renderScreen() } return validOptions[+command](handler) }) }) return input }}

Como puede ver, el initmétodo llama al setupInputmétodo que básicamente configura la devolución de llamada correcta para manejar la entrada del usuario. Esa devolución de llamada tiene la lógica para decidir qué hacer en función de la entrada del usuario (ya sea 1 o 2). Todo sobre Golf

 

Controladores de widgets

Algunos de los widgets (normalmente widgets de entrada) tienen una handlerPathpropiedad que hace referencia al archivo que contiene la lógica detrás de ese componente en particular. Este no es el mismo que el controlador de pantalla anterior. A estos no les importan mucho los componentes de la interfaz de usuario. En cambio, manejan la lógica de unión entre la interfaz de usuario y cualquier biblioteca que estemos usando para interactuar con servicios externos (como la API del motor del juego).

Tipos de widgets

Otra adición menor a la definición JSON de los widgets son sus tipos. En lugar de seguir los nombres que Blessed definió para ellos, estoy creando otros nuevos para darme más margen de maniobra en lo que respecta a su comportamiento. Después de todo, es posible que un widget de ventana no siempre “simplemente muestre información” o que un cuadro de entrada no siempre funcione de la misma manera.

Esto fue principalmente un movimiento preventivo, solo para asegurarme de tener esa capacidad si alguna vez la necesito en el futuro, pero como estás a punto de ver, de todos modos no estoy usando tantos tipos diferentes de componentes.

Múltiples pantallas

Aunque la pantalla principal es la que te mostré en la captura de pantalla anterior, el juego requiere algunas otras pantallas para solicitar cosas como tu nombre de jugador o si estás creando una nueva sesión de juego o incluso uniéndote a una existente. La forma en que lo manejé fue, nuevamente, mediante la definición de todas estas pantallas en el mismo archivo JSON. Y para pasar de una pantalla a la siguiente, usamos la lógica dentro de los archivos del controlador de pantalla.

Podemos hacer esto simplemente usando la siguiente línea de código:

this.UI.loadScreen('main-ui', (err ) = { if(err) this.UI.setUpAlert(err) })

Le mostraré más detalles sobre la propiedad UI en un segundo, pero solo estoy usando ese loadScreenmétodo para volver a representar la pantalla y seleccionando los componentes correctos del archivo JSON usando la cadena pasada como parámetro. Muy sencillo.

Ejemplos de código

Ahora es el momento de revisar el meollo de este artículo: los ejemplos de código. Solo voy a resaltar lo que creo que son las pequeñas joyas que contiene, pero siempre puedes echar un vistazo al código fuente completo directamente en el repositorio en cualquier momento.

Uso de archivos de configuración para generar automáticamente la interfaz de usuario

Ya he cubierto parte de esto, pero creo que vale la pena explorar los detalles detrás de este generador. La esencia detrás de esto (archivo index.js dentro de la /uicarpeta) es que es un contenedor alrededor del objeto Blessed. Y el método más interesante que contiene es el loadScreenmétodo.

 

Este método toma la configuración (a través del módulo de configuración) para una pantalla específica y revisa su contenido, intentando generar los widgets correctos según el tipo de cada elemento.

loadScreen: function(sname, extras, done) { if(typeof extras == "function") { done = extras } let screen = config.get('screens.' + sname) let screenElems = {} if(this.screenElements.length 0) { //remove previous screen this.screenElements.map( e = e.detach()) this.screen.realloc() } Object.keys(screen.elements).forEach( eName = { let elemObj = null let element = screen.elements[eName] if(element.type == 'window') { elemObj = this.setUpWindow(element) } if(element.type == 'input') { elemObj = this.setUpInputBox(element) } if(element.type == 'input-prompt') { elemObj = this.setUpInputBox(element) } screenElems[eName] = { meta: element, obj: elemObj } }) if(typeof extras === 'object' extras.flashmessage) { this.setUpAlert(extras.flashmessage) } this.renderScreen() let logicPath = require(screen.file) logicPath.init(screenElems, this) done() },

Como puedes ver, el código es un poco largo, pero la lógica detrás de él es simple:

  1. Carga la configuración para la pantalla específica actual;
  2. Limpia cualquier widget previamente existente;
  3. Revisa cada widget y lo crea una instancia;
  4. Si se pasó una alerta adicional como un mensaje flash (que es básicamente un concepto que robé de Web Dev en el que configuras un mensaje para que se muestre en la pantalla hasta la próxima actualización);
  5. Renderizar la pantalla real;
  6. Y finalmente, solicite el controlador de pantalla y ejecute su método "init".

¡Eso es todo! Puedes consultar el resto de los métodos: en su mayoría están relacionados con widgets individuales y cómo renderizarlos.

Comunicación entre la interfaz de usuario y la lógica empresarial

Aunque a gran escala, la interfaz de usuario, el back-end y el servidor de chat tienen una comunicación basada en capas; la interfaz en sí necesita al menos una arquitectura interna de dos capas en la que los elementos puros de la interfaz de usuario interactúan con un conjunto de funciones que representan la lógica central dentro de este proyecto en particular.

El siguiente diagrama muestra la arquitectura interna del cliente de texto que estamos creando:

( Vista previa grande )

Déjame explicarlo un poco más. Como mencioné anteriormente, loadScreenMethodcreará presentaciones de interfaz de usuario de los widgets (estos son objetos benditos). Pero están contenidos como parte del objeto lógico de pantalla que es donde configuramos los eventos básicos (como onSubmitlos cuadros de entrada).

Permítanme darles un ejemplo práctico. Aquí está la primera pantalla que ve al iniciar el cliente UI:

( Vista previa grande )

 

Hay tres secciones en esta pantalla:

  1. Solicitud de nombre de usuario,
  2. Opciones de menú/información,
  3. Pantalla de entrada para las opciones del menú.

Básicamente, lo que queremos hacer es solicitar el nombre de usuario y luego pedirles que elijan una de las dos opciones (ya sea iniciar un juego nuevo o unirse a uno existente).

El código que se encarga de eso es el siguiente:

module.exports = { init: function(elements, UI) { this.elements = elements this.UI = UI this.id = "main-options" this.setInput() }, moveToIDRequest: function(handler) { return this.UI.loadScreen('id-requests', (err, ) = { }) }, createNewGame: function(handler) { handler.createNewGame(this.UI.gamestate.APIKEY, (err, gameData) = { this.UI.gamestate.gameID = gameData._id handler.joinGame(this.UI.gamestate, (err) = { return this.UI.loadScreen('main-ui', { flashmessage: "You've joined game " + this.UI.gamestate.gameID + " successfully" }, (err, ) = { }) }) }) }, setInput: function() { let handler = require(this.elements["input"].meta.handlerPath) let input = this.elements["input"].obj let usernameRequest = this.elements['username-request'].obj let usernameRequestMeta = this.elements['username-request'].meta let question = usernameRequestMeta.params.content.trim() usernameRequest.setValue(question) this.UI.renderScreen() let validOptions = { 1: this.moveToIDRequest.bind(this), 2: this.createNewGame.bind(this) } usernameRequest.on('submit', (username) = { logger.info("Username:" +username) logger.info("Playername: " + username.replace(question, '')) this.UI.gamestate.playername = username.replace(question, '') input.focus() input.on('submit', (data) = { let command = input.getValue() if(!validOptions[+command]) { this.UI.setUpAlert("Invalid option: " + command) return this.UI.renderScreen() } return validOptions[+command](handler) }) }) return input }}

Sé que es mucho código, pero concéntrate en el initmétodo. Lo último que hace es llamar al setInputmétodo que se encarga de agregar los eventos correctos a los cuadros de entrada correctos.

Por tanto, con estas líneas:

let handler = require(this.elements["input"].meta.handlerPath)let input = this.elements["input"].objlet usernameRequest = this.elements['username-request'].objlet usernameRequestMeta = this.elements['username-request'].metalet question = usernameRequestMeta.params.content.trim()

Estamos accediendo a los objetos Benditos y obteniendo sus referencias, para luego poder configurar los submiteventos. Entonces, después de enviar el nombre de usuario, cambiaremos el foco al segundo cuadro de entrada (literalmente con input.focus() ).

Dependiendo de la opción que elijamos del menú, llamaremos a cualquiera de los métodos:

  • createNewGame: crea un nuevo juego interactuando con su controlador asociado;
  • moveToIDRequest: muestra la siguiente pantalla encargada de solicitar el ID del juego para unirse.

Comunicación con el motor del juego

Por último, pero no menos importante (y siguiendo el ejemplo anterior), si presionas 2, notarás que el método createNewGameusa los métodos del controlador createNewGamey luego joinGame(unirse al juego justo después de crearlo).

 

Ambos métodos están destinados a simplificar la interacción con la API de Game Engine. Aquí está el código para el controlador de esta pantalla:

const request = require("request"), config = require("config"), apiClient = require("./apiClient")let API = config.get("api")module.exports = { joinGame: function(apikey, gameId, cb) { apiClient.joinGame(apikey, gameId, cb) }, createNewGame: function(apikey, cb) { request.post(API.url + API.endpoints.games + "?apikey=" + apikey, { //creating game body: { cartridgeid: config.get("app.game.cartdrigename") }, json: true }, (err, resp, body) = { cb(null, body) }) }}

Allí verá dos formas diferentes de manejar este comportamiento. El primer método en realidad usa la apiClientclase, que nuevamente envuelve las interacciones con GameEngine en otra capa más de abstracción.

Sin embargo, el segundo método realiza la acción directamente enviando una solicitud POST a la URL correcta con la carga útil correcta. Después no se hace nada sofisticado; simplemente enviamos el cuerpo de la respuesta a la lógica de la interfaz de usuario.

Nota : si está interesado en la versión completa del código fuente de este cliente, puede consultarlo aquí .

Ultimas palabras

Esto es todo para el cliente basado en texto para nuestra aventura de texto. Cubrí:

  • Cómo estructurar una aplicación cliente;
  • Cómo utilicé Blessed como tecnología central para crear la capa de presentación;
  • Cómo estructurar la interacción con los servicios back-end de un cliente complejo;
  • Y con suerte, con el repositorio completo disponible.

Y aunque es posible que la interfaz de usuario no se vea exactamente como la versión original, cumple su propósito. Con suerte, este artículo le dio una idea de cómo diseñar un proyecto de este tipo y se sintió inclinado a probarlo usted mismo en el futuro. Blessed es definitivamente una herramienta muy poderosa, pero tendrás que tener paciencia mientras aprendes a usarla y a navegar por sus documentos.

En la siguiente y última parte, cubriré cómo agregué el servidor de chat tanto en el back-end como para este cliente de texto.

¡Nos vemos en la próxima!

Otras partes de esta serie

  • Parte 1 : La Introducción
  • Parte 2 : Diseño del servidor Game Engine
  • Parte 4 : Agregar chat a nuestro juego

(dm, yk, il)Explora más en

  • Nodo.js
  • javascript





Tal vez te puede interesar:

  1. ¿Deberían abrirse los enlaces en ventanas nuevas?
  2. 24 excelentes tutoriales de AJAX
  3. 70 técnicas nuevas y útiles de AJAX y JavaScript
  4. Más de 45 excelentes recursos y repositorios de fragmentos de código

Escribir un motor de aventuras de texto multijugador en Node.js: crear el cliente terminal (Parte 3)

Escribir un motor de aventuras de texto multijugador en Node.js: crear el cliente terminal (Parte 3)

Índice Revisando el diseño original Las herra

programar

es

https://aprendeprogramando.es/static/images/programar-escribir-un-motor-de-aventuras-de-texto-multijugador-en-node-1006-0.jpg

2024-05-21

 

Escribir un motor de aventuras de texto multijugador en Node.js: crear el cliente terminal (Parte 3)
Escribir un motor de aventuras de texto multijugador en Node.js: crear el cliente terminal (Parte 3)

Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

 

 

Top 20