Las computadoras de Internet ofrecen varias ventajas sobre las plataformas tradicionales de computación en la nube que brindan una experiencia de desarrollo de aplicaciones más optimizada.

Soy ingeniero en DFINITY, pero también soy desarrollador de software, así que quería probar esta premisa y evaluar la experiencia de construir en una computadora con Internet desde la perspectiva de un desarrollador web.

Elegí construir una versión reversible, un juego de mesa de estrategia para dos jugadores, no como una aplicación de muestra sino como una aplicación real con todas las posibilidades y detalles que imaginaba que tendría un juego reversible multijugador.

Antes de profundizar en los detalles técnicos detrás de escena, quiero centrarme en el concepto de alto nivel: un entorno virtual donde las aplicaciones de Internet pueden conectarse entre sí sin problemas.

Personalmente creo que a medida que se desarrolle la computación en la nube, la infraestructura se convertirá en una mercancía. En otras palabras, ya no importa quién proporcione la infraestructura.

Lo importante es: escribes una aplicación y se ejecuta en Internet.

modelo de programación

La experiencia de desarrollar aplicaciones web en una computadora con Internet era similar a la de plataformas más nuevas como (ahora desaparecida) Parse o plataformas similares.

La premisa básica de dicha plataforma es ocultar la complejidad de crear y mantener servicios backend (como servidores HTTP, bases de datos, inicios de sesión de usuarios, etc.).

En cambio, proporcionan un entorno virtual abstracto que sólo ejecuta aplicaciones de usuario, sin que los usuarios sepan o tengan que prestar atención a dónde y cómo se ejecutan sus aplicaciones.

Desde esta perspectiva, las computadoras de Internet son a la vez familiares y diferentes.

El elemento básico de las aplicaciones informáticas de Internet es un contenedor, que conceptualmente es un proceso que se ejecuta en tiempo real y que:

  • es 100% determinista (si todas las entradas y estados son iguales, la salida debe ser la misma)

  • Persistencia transparente (también llamada persistencia ortogonal)

  • Comunicarse con usuarios u otros contenedores a través de mensajes asincrónicos (llamadas a funciones remotas)

  • Procesar un mensaje a la vez (según el modelo de actor)

Si pensamos que los contenedores Docker virtualizan un sistema operativo (SO) completo, un contenedor virtualiza un solo programa, ocultando casi todos los detalles del sistema operativo.

Parece demasiado restrictivo ya que no ejecutará su sistema operativo o base de datos favoritos. ¿Para qué se utiliza?

Personalmente prefiero pensar en términos de disciplinas más que de limitaciones, solo para resaltar dos propiedades (entre muchas) que hacen que el modelo de contenedor sea diferente de los servicios web habituales:

  • Atomicidad: las actualizaciones de estado de cada archivo jar son atómicas (llamadas a funciones remotas), la llamada se realiza correctamente y el estado se actualiza, o se genera un error y el estado no se modifica (como si la llamada nunca hubiera ocurrido).

  • Mensajería bidireccional: los mensajes se entregan como máximo una vez y la persona que llama al mensaje siempre tiene garantizada una respuesta exitosa o fallida.

Es difícil obtener dicha garantía sin limitar la funcionalidad del programa de usuario.

Con suerte, al final de este artículo, estará de acuerdo en que el modelo de contenedor restringido puede lograr mucho al encontrar la combinación óptima de eficiencia, robustez y simplicidad.

Cliente: Arquitectura del servidor

Los juegos multijugador requieren el intercambio de datos entre jugadores y su implementación suele seguir una arquitectura cliente-servidor:

  • El servidor aloja el juego real y gestiona la comunicación con los clientes del juego.

  • Dos o más clientes (cada uno de los cuales representa a un jugador) obtienen el estado del servidor, representan la interfaz de usuario del juego y también aceptan la entrada del jugador para reenviarla al servidor.

Crear un juego multijugador como una aplicación web significa que el cliente debe ejecutarse en un navegador, utilizando el protocolo HTTP para la comunicación de datos y Javascript (JS) para representar la interfaz de usuario del juego como una página web.

Para este juego de inversión multijugador, quiero implementar la siguiente funcionalidad:

  • Dos jugadores cualesquiera pueden elegir jugar uno contra el otro.

  • Los jugadores ganan puntos al ganar juegos, que también cuentan para su puntuación acumulada.

  • Marcador que muestra los mejores jugadores

  • Y, por supuesto, está el flujo habitual del juego: recibir información de cada jugador por turno, aplicar solo movimientos legales y detectar el final del juego para calcular los puntos.

Gran parte de esta lógica de juego tiene que ver con la manipulación del estado, y la implementación del lado del servidor ayuda a garantizar que los jugadores tengan una visión coherente.

servidor de fondo

En una configuración de backend tradicional, tendría que elegir un conjunto de software del lado del servidor, incluida una base de datos para almacenar datos de jugadores y juegos, un servidor web para manejar solicitudes HTTP y luego escribir mi propio software de aplicación para combinar los dos. implementar un conjunto completo de lógica del lado del servidor.

En una configuración "sin servidor", normalmente la plataforma ya proporciona servicios de base de datos y servidor web, y solo necesito escribir un software de aplicación que llame a la plataforma para utilizar estos servicios.

A pesar del término engañoso "sin servidor", la aplicación seguirá desempeñando el papel de "servidor" según lo dicta la arquitectura cliente-servidor.

Independientemente de la configuración del backend, la pieza central del diseño de mi aplicación es un conjunto de API que controlan la comunicación entre el servidor del juego y sus clientes.

Desarrollar esta aplicación en una computadora con Internet no es diferente, así que comencé con el siguiente diseño de alto nivel del flujo del juego:

Después de que los jugadores se hayan registrado, si dos de ellos expresan su deseo de jugar entre ellos, se iniciará un nuevo juego llamando a start(opponent_name).

Luego, los jugadores se turnan para realizar la siguiente acción, y el otro jugador tendrá que llamar periódicamente a view() para actualizar su vista en el último estado del juego, luego realizar la siguiente acción y así sucesivamente hasta que termine el juego.

Como regla general, los jugadores sólo pueden jugar un juego a la vez.

El servidor debe conservar los siguientes conjuntos de datos:

  • Listado de jugadores registrados, sus nombres y puntuaciones, etc.

  • Lista de juegos en curso, cada juego incluye el tablero de juego más reciente, quién juega el juego en blanco y negro, quién puede moverse a continuación y el resultado final después de completar el juego, etc.

Elegí implementar el servidor en Motoko, pero en teoría cualquier lenguaje que pueda compilarse en Web Assembly (Wasm) debería funcionar bien siempre que utilice la misma API del sistema para comunicarse con los componentes de Internet. (En el momento de escribir este artículo, el SDK de Rust está a punto de lanzarse).

Como nuevo lenguaje, Motoko tiene algunas ventajas generales (por ejemplo, su biblioteca base es un poco escasa y aún no es estable), pero ya tiene soporte para el administrador de paquetes y el protocolo de servidor de idiomas (LSP) en VSCode, lo que hace que el proceso de desarrollo se convierta en bastante agradable (esto se debe a que soy usuario de Vim).

En este artículo, no hablaré del lenguaje Motoko en sí.

En lugar de eso, discutiré algunas de las características notables de Motoko e Internet Computers que hacen que el desarrollo de contenedores sea emocionante.

variable estable

La persistencia ortogonal (OP) no es una idea nueva.

Las nuevas generaciones de hardware informático, como NVRam, han eliminado en gran medida la barrera al almacenamiento persistente de toda la memoria de los programas, y el acceso al almacenamiento externo, como los sistemas de archivos, se ha vuelto opcional para los programas.

Sin embargo, un desafío que se menciona a menudo en la literatura de OP tiene que ver con las actualizaciones, es decir, ¿qué sucede cuando una actualización tiene que cambiar las estructuras de datos del programa o el diseño de la memoria?

Motoko respondió a esta pregunta con variables estables. Pueden sobrevivir a las actualizaciones, lo que en mi opinión es ideal para guardar los datos de los jugadores, ya que no quiero que los jugadores pierdan sus cuentas al actualizar el software contenedor.

En el desarrollo normal del lado del servidor, tengo que almacenar las cuentas de los jugadores en un archivo o base de datos, que es un servicio básico del sistema para plataformas "sin servidor".

Sólo ciertos tipos de variables son estables, pero por lo demás son como cualquier otra variable que almacena datos en el montón y puede usarse como tal.

Dicho esto, actualmente existe una limitación en el uso de HashMaps como variables estables, por lo que tengo que recurrir a matrices. Aquí hay un ejemplo:

Espero que una versión futura del SDK de DFINITY elimine esta limitación para poder usar simplemente un reproductor var estable sin ninguna conversión.

Autenticación de usuario

Cada contenedor y cada cliente (por ejemplo, línea de comando dfx o navegador) obtendrán una ID principal que los identifica de forma única (para los clientes, dichas ID se generan automáticamente a partir de pares de claves públicas/privadas, y la biblioteca DFINITY JS las administra y actualmente reside en el navegador). almacenamiento local).

Motoko permite que el contenedor identifique a la persona que llama a la función "compartida", que podemos usar con fines de autenticación.

Por ejemplo, defino las funciones de registro y visualización de la siguiente manera:

La expresión msg.caller proporciona el ID principal de la persona que llama del mensaje. Tenga en cuenta que es diferente de la persona que llama de la función.

En Motoko, los mensajes a los actores deben enviarse a una función de acceso público, que debe tener un tipo de devolución asincrónica.

El código anterior muestra dos funciones públicas: registrar y ver, donde esta última es una llamada de consulta, marcada por la palabra clave de consulta.

Como hemos visto, acceder al campo del llamante del mensaje debe utilizar una sintaxis especial: compartido(msg) o consulta compartida(msg), donde msg es un parámetro formal que se refiere al mensaje entrante en su conjunto.

Actualmente, el único atributo que tiene es el de persona que llama.

Poder acceder al ID único de la persona que llama (remitente del mensaje) resulta familiar, como una cookie HTTP.

Pero a diferencia de HTTP, el Protocolo informático de Internet en realidad garantiza que la identificación del sujeto sea criptográficamente segura y que los programas de usuario que se ejecutan en las computadoras de Internet puedan tener total confianza en su autenticidad.

Personalmente, creo que hacer que el programa sepa quién llama probablemente sea demasiado poderoso y demasiado rígido (por ejemplo, ¿qué sucede cuando es necesario cambiar dicha identificación?).

Pero por ahora, conduce a un esquema de autenticación muy simple que los desarrolladores de aplicaciones pueden aprovechar, y espero ver más desarrollo en esta área.

Concurrencia y atomicidad

Los clientes del juego pueden enviar mensajes al servidor del juego en cualquier momento, por lo que es responsabilidad del servidor manejar correctamente las solicitudes simultáneas.

En una arquitectura normal, tendría que crear cierta lógica para determinar el orden en el que los jugadores se mueven (normalmente a través de una cola de paso de mensajes o un mutex).

A través del modelo de programación de actores utilizado por el contenedor, este problema se resuelve automáticamente sin que yo tenga que escribir ningún código.

Los mensajes son solo llamadas a funciones remotas y se garantiza que el contenedor procesará solo un mensaje a la vez. Esto da como resultado una lógica de programación simplificada y no tengo que preocuparme en absoluto de que se llamen funciones simultáneamente.

Debido a que el estado del contenedor solo se retiene después de que un mensaje se haya procesado por completo (es decir, la llamada a la función pública regresa), no tengo que preocuparme por vaciar la memoria en el disco, ya sea que las excepciones causen corrupción del estado del disco o estén relacionadas con la confiabilidad.

También tenga en cuenta que los cambios de estado persistentes son atómicos por mensaje.

Las funciones públicas pueden llamar libremente a cualquier otra función no asíncrona y el estado modificado se conserva siempre que toda la ejecución se complete sin errores (para llamadas de actualización, más detalles a continuación).

Se puede lograr una granularidad más fina emitiendo llamadas asincrónicas en lugar de llamadas sincrónicas, que se convierten en nuevos mensajes que el sistema debe programar en lugar de ejecutar inmediatamente.

Si tuviera que construir este juego usando una arquitectura convencional, probablemente también elegiría un marco de actor, como Akka de Java, Actix de Rust, etc.

Motoko ofrece soporte para actores nativos, uniéndose a la familia de lenguajes de programación basados ​​en actores como Erlang y Pony.

Actualizar llamadas y consultar llamadas

Creo que esta característica realmente podría mejorar la experiencia del usuario para las aplicaciones informáticas de Internet y las pone a la par de lo que alojan las plataformas de nube tradicionales (y órdenes de magnitud más rápidas en comparación con otras cadenas de bloques).

También es un concepto simple: cualquier función pública que no requiera cambiar el estado del programa se puede marcar como una llamada de "consulta"; de lo contrario, se trata como una llamada de "actualización" de forma predeterminada.

La diferencia entre consultas y actualizaciones es la latencia y la concurrencia:

  • Una llamada de consulta puede tardar solo unos milisegundos en completarse, mientras que una llamada de actualización tarda unos dos segundos.

  • Las llamadas de consulta se pueden ejecutar simultáneamente y escalar bien, las llamadas de actualización se realizan de forma secuencial (según el modelo de actor) y brindan garantías de atomicidad.

Al igual que en el ejemplo de código anterior, pude marcar la función de vista como una llamada de consulta porque simplemente busca y devuelve el estado del juego en el que está jugando el jugador.

De hecho, la mayor parte del tiempo que navegamos por la web, realizamos llamadas de consulta: los datos se recuperan del servidor pero no se modifican.

Por otro lado, la función de registro anterior se mantiene como una llamada de actualización ya que debe agregar el nuevo jugador a la lista de jugadores después de un registro exitoso.

Las llamadas de actualización tardarán más por muchos motivos, como la coherencia, la atomicidad y la confiabilidad de los datos.

Pero éste no es un problema inherente a las computadoras con Internet.

Muchas acciones en la web hoy en día tardan más de dos segundos en completarse, como pagar con tarjeta de crédito, realizar un pedido o iniciar sesión en una cuenta bancaria, por nombrar algunas.

Creo que dos segundos es el punto de inflexión para una buena experiencia de usuario.

Volviendo al juego inverso, cuando el jugador haga su siguiente movimiento, también debe ser una llamada de actualización:

Si un juego solo actualiza su pantalla dos segundos después de que un jugador hace clic con el mouse (o toca la pantalla), sentirá que no responde y nadie querrá jugar un juego en tan mal momento.

Así que tuve que optimizar esta parte reaccionando a la entrada del usuario directamente en el lado del cliente sin tener que esperar a que el servidor respondiera.

Esto significa que la interfaz de usuario tendrá que validar los movimientos del jugador, calcular qué piezas se voltearán y mostrarlas en la pantalla inmediatamente.

Esto también significa que cualquier cosa que la interfaz muestre al reproductor, cuando regrese, debe coincidir con la respuesta del servidor a la misma acción; de lo contrario, corremos el riesgo de encontrarnos con inconsistencias.

Pero, de nuevo, creo que cualquier implementación razonable de un juego de ajedrez o multijugador bidireccional puede hacer esto, independientemente de si su backend tarda 200 ms en responder o 2 segundos.

cliente front-end

DFINITY SDK proporciona una interfaz que carga aplicaciones directamente en el navegador.

Sin embargo, es diferente de las páginas HTML normales servidas por servidores web.

La comunicación con el contenedor backend se realiza a través de llamadas a funciones remotas, que en el caso de los navegadores se superponen a HTTP.

Esto lo maneja de forma transparente la biblioteca de usuario JS, por lo que un programa JS simplemente importa el contenedor como un objeto JS y puede llamar a sus funciones públicas al igual que las funciones JS asincrónicas normales del objeto.

El SDK de DFINITY tiene un conjunto de tutoriales sobre cómo configurar una interfaz JS, por lo que no entraré en detalles aquí.

Detrás de escena, el comando dfx en el SDK usa Webpack para agrupar recursos que incluyen JS, CSS, imágenes y otros archivos que pueda tener.

También puede combinar sus marcos JS favoritos (como React, AngularJS, Vue.js, etc.) con la biblioteca de usuario DFINITY para desarrollar una interfaz JS para usar en navegadores o aplicaciones móviles.

Componentes principales de la interfaz de usuario

Soy relativamente nuevo en el desarrollo front-end y solo tengo una breve experiencia con React.

Esta vez me tomé la libertad de aprender Mithril porque había oído muchas cosas buenas sobre Mithril, especialmente su simplicidad.

En aras de la simplicidad, también se me ocurrió un diseño con sólo dos pantallas:

  • Una pantalla "Jugar" que permite a los jugadores ingresar su propio nombre y el nombre de su oponente antes de ingresar a la pantalla "Juego". También mostrará algunos consejos e instrucciones, un gráfico de los mejores jugadores, jugadores recientes y más.

  • Una pantalla de "juego" que acepta la entrada del jugador y se comunica con el contenedor backend para representar un tablero inverso. También mostrará la puntuación del jugador al final del juego y luego llevará al jugador de regreso a la pantalla del Juego.

El siguiente fragmento de código muestra el marco de la interfaz del juego JS:

Hay algunas cosas a tener en cuenta:

  • Al igual que cualquier otra biblioteca JS, se importa el contenedor backend principal. Piense en ello como un proxy que reenvía llamadas a funciones a un servidor remoto, recibe respuestas y maneja de forma transparente la autenticación necesaria, la firma de mensajes, la serialización/deserialización de datos, la propagación de errores, etc.

  • También se importará otro contenedor reversi_assets. Esta es una forma de incluir los activos necesarios con Webpack al instalar el contenedor backend. En este caso tengo un archivo de sonido que se reproducirá cuando el reproductor coloque una nueva pieza.

  • Una imagen de logotipo que va directamente a él. Esto debe configurarse en Webpack usando url-loader, que es una herramienta que en realidad incrusta el contenido de la imagen como una cadena Base64 que se usará para el elemento de imagen. Funciona para imágenes pequeñas pero no para imágenes grandes.

  • La aplicación final se configura usando Mithril a través de las dos rutas /play y /game. Este último toma los nombres del jugador y del oponente como dos parámetros, lo que permite recargar la pantalla del juego en el navegador sin interrumpir el juego.

Cargar recursos desde el contenedor de activos

Como soy nuevo en la carga de elementos DOM de forma asincrónica en JS, me he esforzado un poco en esto.

Cuando DFX construye el jar, también crea un jar reversi_assets, que básicamente simplemente empaqueta todo en src/reversi_assets/assets/ en él.

Utilizo esto para recuperar un archivo de sonido, pero cargarlo correctamente no es tan sencillo como colocar la URL del archivo mp3 en el campo src del elemento HTML.

Así es como lo cargo (si eres desarrollador front-end, probablemente ya lo sepas):

Cuando se llama a la función de inicio (desde el contexto asíncrono), intentará recuperar el archivo "put.mp3" del contenedor remoto.

Después de una recuperación exitosa, utilizará la herramienta JS AudioContext para decodificar los datos de audio e inicializar la variable global putsound.

Si putsound se inicializa correctamente, una llamada a playAudio(putsound) reproducirá el sonido real:

Otros recursos se pueden cargar de manera similar. No estoy usando ninguna imagen más que el logotipo, que es pequeño y su código fuente se puede incrustar en Webpack agregando la siguiente configuración a webpack.config.js:

formato de intercambio de datos

El concepto de Motoko son datos "compartibles", es decir, datos que se pueden enviar a través de límites de contenedores o idiomas.

Obviamente, no me imagino que un puntero de montón en C sea "compartible", pero para mí, cualquier cosa que pueda asignarse a JSON se puede "compartir".

Para ello, DFINITY desarrolló un IDL (Lenguaje de descripción de interfaz) llamado Candid para aplicaciones informáticas de Internet.

Candid simplifica enormemente la forma en que el front-end se comunica con el back-end o entre contenedores.

Por ejemplo, aquí hay un fragmento (incompleto) del contenedor reversible backend descrito por Candid:

Tome el método de movimiento como ejemplo:

  • Este es uno de los métodos exportados en la interfaz de servicio del contenedor.

  • Toma como entrada dos números enteros (que representan una coordenada) y devuelve un resultado de tipo MoveResult.

  • MoveResult es una variante (también conocida como enumeración) que representa los resultados y errores que pueden ocurrir cuando el jugador se mueve.

  • En las distintas ramas de MoveResult, GameOver indica que el juego está completo y toma un parámetro ColorCount, que representa la cantidad de piezas blancas y negras en el tablero de juego.

El código fuente de Motoko genera automáticamente un archivo Candid para cada contenedor y la biblioteca de usuario JS lo utiliza automáticamente sin la participación del desarrollador:

  • Del lado de Motoko, cada tipo Candid corresponde a un tipo Motoko y cada método corresponde a una función pública.

  • En el lado JS, cada tipo Candid corresponde a un objeto JSON y cada método corresponde a una función miembro del objeto contenedor importado.

La mayoría de los tipos Candid tienen representación JS directa, algunos requieren cierta conversión.

Por ejemplo, nat es una precisión arbitraria tanto en Motoko como en Candid, en JS está asignado a un entero bignumber.js, por lo que debe convertirse al tipo de número nativo de JS usando n.toNumber().

Un problema que tengo es con los valores nulos en Candid (y el tipo de opción de Motoko).

Se representa en JSON como una matriz vacía [], en lugar de su valor nulo nativo. Esto es para distinguir el caso en el que tenemos opciones anidadas, como Opción >:

Candid es muy poderoso, aunque en la superficie suena mucho a Protocolbuf o JSON.

Entonces ¿por qué es necesario?

Hay muchas buenas razones más allá de lo que se presenta aquí, y animo a cualquiera interesado en este tema a leer Candid Spec.

Sincronizar el estado del juego con el backend

Como mencioné antes, utilicé un truco para reaccionar inmediatamente a la entrada válida del usuario sin tener que esperar a que respondiera el servidor backend del juego.

Esto significa que la interfaz solo necesita el reconocimiento del servidor del juego (o, si corresponde, el manejo de errores) después de que el jugador se mueve.

Además de enviar sus propios movimientos, el cliente también debe conocer los movimientos del otro jugador.

Esto se logra llamando periódicamente a la función view() del contenedor del juego alojado en el lado del servidor.

La implicación de este diseño es que tengo que repetir parte de la misma lógica del juego en el backend (Motoko) y el frontend (JS), lo cual no es ideal.

Dado que Motoko se puede compilar en Wasm y Wasm se puede ejecutar en el navegador, ¿no sería fantástico si tanto el front-end como el back-end pudieran compartir el mismo módulo Wasm que implementa la lógica central del juego? Este tipo de intercambio solo comparte código, no estado.

Puede que requiera cierta configuración, pero creo que es completamente posible y puedo intentarlo en una actualización futura.

Especialmente con el juego inverso, en algunos casos se puede impedir que un jugador realice cualquier acción, por lo que el otro jugador puede realizar dos acciones consecutivas o incluso más.

Para mostrar cada movimiento realizado por el jugador, elegí implementar el estado del juego como una secuencia de acciones en lugar de solo el último estado del tablero de juego.

Esto también significa que al comparar la lista de acciones en el estado local del frontend con lo que se devuelve al llamar a la función view(), podemos saber fácilmente qué ha sucedido desde la última acción del jugador (es el turno del jugador de dar el siguiente paso). , etc.

animación SVG

Es posible que el tema de la animación utilizando gráficos vectoriales escalables (SVG) no pertenezca a este artículo, pero una vez realmente me quedé atascado en él.

Por eso quiero compartir las lecciones que aprendí.

El problema que tengo es que cuando uso repetirCount para configurar la animación para que se muestre solo una vez, la animación no comienza.

La mayoría de los recursos en línea en SVG solo proporcionan ejemplos que se pueden repetir infinitamente o usar una configuración de repetición de cuenta.

Implícitamente asumen que si la animación solo se muestra una vez, comienza después de que se carga la página (o se establece algún retraso).

Sin embargo, con la mayoría de los marcos de aplicaciones de una página como React o Mithril, la página generalmente no se recarga, sino que simplemente se vuelve a representar.

Entonces, cuando quiero mostrar un fragmento de juego cambiando de blanco a negro o de negro a blanco, tiene que suceder cuando la página se vuelve a renderizar, no cuando se recarga.

Me perdí esta diferencia clave y sólo la descubrí después de probarla muchas veces.

Así es como uso Mithril para representar un elemento animado (como hijo de un SVG) donde el rx de la elipse cambia del radio inicial a 0 y viceversa.

La explicación es la siguiente:

  • El inicio está configurado en indeterminado para que la animación se pueda controlar/iniciar manualmente.

  • El relleno está configurado para congelarse, lo que significa que una vez finalizada la animación, su estado final permanecerá sin cambios.

  • Los valores están establecidos en 4 valores donde los dos primeros se repiten como un truco para iniciar la animación después de un retraso de 0.1s (1/4 de dur), esto se debe a que el inicio está establecido en indefinido.

El punto principal es que la animación debe iniciarse manualmente. Lo activo con un retraso de 0 segundos usando setTimeout, un truco que espera hasta que el nuevo elemento de la interfaz de usuario preparado por Mithril se represente en el DOM del navegador:

Como se mencionó anteriormente, cualquier elemento animado cuyo ID no comience con "punto" se iniciará inmediatamente.

proceso de desarrollo

Desarrollé el juego en Linux y la configuración inicial consistió en instalar el SDK de DFINITY y seguir sus instrucciones para crear el proyecto.

Recordar todas las líneas de comando dfx es engorroso, así que creé un Makefile para ayudar.

La depuración y las pruebas se realizan principalmente en el navegador, por lo que se requiere una gran cantidad de console.log().

En realidad, existe una manera de escribir pruebas unitarias en Motoko, pero solo la aprendí después de escribir un juego.

Inicialmente también desarrollé una interfaz basada en terminal usando scripts de shell y dfx.

Creo que esto ayuda a acelerar la depuración sin tener que pasar por el navegador.

Pero, por supuesto, las pruebas unitarias son una mejor manera de garantizar la corrección.

¡Juega!

Para poder ejecutar este juego en una computadora con Internet, ahora existe una red Tungsten abierta a desarrolladores externos.

Te animo a que te registres, clones este proyecto e implementes el juego tú mismo para obtener experiencia de primera mano como desarrollador.

Pero los no desarrolladores no pueden acceder a la aplicación en Tungsten porque aún no es pública.

Así que también lo hospedé yo mismo usando dfx y nginx como proxy inverso para poder invitar a amigos a jugar.

No animaría a la gente a que lo hicieran ellos mismos ya que el software aún se encuentra en la etapa Alpha.

Este es un enlace al juego real, sólo para fines de demostración. Mi plan es implementarlo en una red informática pública de Internet una vez que se lance a finales de este año.

Si tiene alguna pregunta, no dude en visitar el repositorio del proyecto y enviar un problema. ¡Las solicitudes de extracción también son bienvenidas!

Únase a nuestra comunidad de desarrolladores y comience a construir en forum.dfinity.org.

Contenido IC que te interesa

Progreso tecnológico | Información del proyecto | Eventos globales

Recopile y siga el canal IC Binance

Manténgase actualizado con la información más reciente