Os computadores da Internet oferecem diversas vantagens em relação às plataformas tradicionais de computação em nuvem, que proporcionam uma experiência de desenvolvimento de aplicativos mais simplificada.
Sou engenheiro na DFINITY, mas também sou desenvolvedor de software, então queria testar essa premissa e avaliar a experiência de construir em um computador na Internet do ponto de vista de um desenvolvedor web.
Optei por construir uma versão reversível, um jogo de estratégia para dois jogadores, não como um aplicativo de exemplo, mas como um aplicativo real com todas as possibilidades e detalhes que imaginei que um jogo multiplayer reversível tivesse.
Antes de mergulhar nos detalhes técnicos dos bastidores, quero focar no conceito de alto nível: um ambiente virtual onde os aplicativos da Internet podem se conectar perfeitamente entre si.
Pessoalmente, acredito que à medida que a computação em nuvem se desenvolve, a infraestrutura se tornará uma mercadoria. Por outras palavras, já não importa quem fornece a infra-estrutura.
O importante é: você escreve uma aplicação e ela roda na Internet.
modelo de programação
A experiência de desenvolvimento de aplicações web em um computador com Internet foi próxima à de plataformas mais recentes como o (agora extinto) Parse ou plataformas semelhantes.
A premissa básica de tal plataforma é ocultar a complexidade de construção e manutenção de serviços de back-end (como servidores HTTP, bancos de dados, logins de usuários, etc.).
Em vez disso, eles fornecem um ambiente virtual abstrato que executa apenas aplicativos de usuário, sem que os usuários saibam ou tenham que prestar atenção onde e como seus aplicativos estão sendo executados.
Dessa perspectiva, os computadores da Internet são familiares e diferentes.
O alicerce básico dos aplicativos de computador da Internet é um contêiner, que é conceitualmente um processo de execução em tempo real que:
é 100% determinístico (se todas as entradas e estados forem iguais, a saída deve ser a mesma)
Persistência transparente (também chamada de persistência ortogonal)
Comunique-se com usuários ou outros contêineres por meio de mensagens assíncronas (chamadas de função remota)
Processe uma mensagem por vez (de acordo com o modelo do ator)
Se pensarmos nos contêineres Docker como a virtualização de um sistema operacional (SO) inteiro, um contêiner virtualiza um único programa, ocultando quase todos os detalhes do sistema operacional.
Parece muito restritivo, pois não executa seu sistema operacional ou banco de dados favorito. para que serve isto?
Pessoalmente, prefiro pensar em termos de disciplinas e não de limitações, apenas para destacar duas propriedades (entre muitas) que tornam o modelo de contêiner diferente dos serviços web regulares:
Atomicidade: as atualizações de estado de cada jar de mensagem são atômicas (chamadas de função remota), a chamada é bem-sucedida e o estado é atualizado ou um erro é gerado e o estado não é tocado (como se a chamada nunca tivesse acontecido).
Mensagens bidirecionais: as mensagens são entregues no máximo uma vez e o chamador da mensagem sempre tem a garantia de uma resposta com sucesso ou falha.
É difícil obter tal garantia sem limitar a funcionalidade do programa do usuário.
Esperamos que, ao final deste artigo, você concorde que o modelo de contêiner restrito pode realmente realizar muito ao encontrar a combinação ideal de eficiência, robustez e simplicidade.
Cliente: Arquitetura do Servidor
Os jogos multiplayer requerem a troca de dados entre os jogadores, e sua implementação geralmente segue uma arquitetura cliente-servidor:
O servidor hospeda o jogo real e gerencia a comunicação com os clientes do jogo
Dois ou mais clientes (cada um representando um jogador) obtêm o estado do servidor, renderizam a UI do jogo e também aceitam a entrada do jogador para encaminhar ao servidor
Construir um jogo multijogador como uma aplicação web significa que o cliente deve ser executado em um navegador, utilizando o protocolo HTTP para comunicação de dados e usando Javascript (JS) para renderizar a UI do jogo como uma página web.
Para este jogo de reversão multijogador, quero implementar a seguinte funcionalidade:
Quaisquer dois jogadores podem escolher jogar um contra o outro
Os jogadores ganham pontos ao vencer jogos, que também contam para a pontuação acumulada
Placar mostrando os melhores jogadores
E, claro, há o fluxo normal do jogo: receber informações de cada jogador, aplicando apenas movimentos legais e detectando o final do jogo para calcular pontos.
Grande parte da lógica do jogo gira em torno da manipulação de estado, e a implementação no lado do servidor ajuda a garantir que os jogadores tenham uma visão consistente.
servidor back-end
Em uma configuração de back-end tradicional, eu teria que escolher um conjunto de software do lado do servidor, incluindo um banco de dados para armazenar dados de jogadores e jogos, um servidor web para lidar com solicitações HTTP e, em seguida, escrever meu próprio software de aplicação para combinar os dois. implementar um conjunto completo de lógica do lado do servidor.
Em uma configuração "sem servidor", normalmente a plataforma já fornece serviços de servidor web e de banco de dados, e eu só preciso escrever um software aplicativo que chame a plataforma para usar esses serviços.
Apesar do termo enganoso “sem servidor”, o aplicativo ainda desempenhará o papel de “servidor”, conforme ditado pela arquitetura cliente-servidor.
Independentemente da configuração de back-end, a peça central do design do meu aplicativo é um conjunto de APIs que controlam a comunicação entre o servidor do jogo e seus clientes.
Desenvolver esta aplicação em um computador com internet não é diferente, então comecei com o seguinte design de alto nível do fluxo do jogo:

Após o registro dos jogadores, se dois deles expressarem o desejo de jogar um com o outro, por meio da chamada start(opponent_name), um novo jogo será iniciado.
Os jogadores então se revezam para definir a próxima ação, e o outro jogador terá que chamar view() periodicamente para atualizar sua visualização no estado mais recente do jogo, depois realizar a próxima ação e assim por diante até o jogo terminar.
Como regra geral, os jogadores só podem jogar um jogo por vez.
O servidor deve reter os seguintes conjuntos de dados:
Lista de jogadores registrados, seus nomes e pontuações, etc.
Lista de jogos em andamento, cada jogo inclui o tabuleiro de jogo mais recente, quem está jogando o jogo preto e branco, quem pode jogar em seguida e o resultado final após completar o jogo, etc.
Optei por implementar o servidor em Motoko, mas em teoria qualquer linguagem que possa compilar para Web Assembly (Wasm) deve funcionar bem desde que utilize a mesma API do sistema para se comunicar com os componentes da internet. (No momento em que este livro foi escrito, o Rust SDK estava prestes a ser lançado.)
Por ser uma nova linguagem, Motoko tem algumas vantagens aproximadas (por exemplo, sua biblioteca base é um pouco deficiente e ainda não estável), mas já possui gerenciador de pacotes e suporte a Language Server Protocol (LSP) no VSCode, o que torna o desenvolvimento O processo tornou-se bastante agradável (isso porque sou usuário do Vim).
Neste artigo, não discutirei a linguagem Motoko em si.
Em vez disso, discutirei alguns dos recursos notáveis do Motoko e dos computadores para Internet que tornam o desenvolvimento de contêineres interessante.
variável estável
A persistência ortogonal (OP) não é uma ideia nova.
As novas gerações de hardware de computador, como o NVRam, removeram em grande parte a barreira ao armazenamento persistente de toda a memória do programa, e o acesso ao armazenamento externo, como sistemas de arquivos, tornou-se opcional para os programas.
No entanto, um desafio frequentemente mencionado na literatura OP diz respeito às atualizações, ou seja, o que acontece quando uma atualização tem que alterar as estruturas de dados ou o layout da memória do programa?
Motoko respondeu a esta pergunta com variáveis estáveis. Eles podem sobreviver a atualizações, o que na minha opinião é ideal para salvar os dados dos jogadores, já que não quero que os jogadores percam suas contas ao atualizar o software do contêiner.
No desenvolvimento regular do lado do servidor, preciso armazenar as contas dos jogadores em um arquivo ou banco de dados, que é um serviço básico do sistema para plataformas "sem servidor".
Apenas certos tipos de variáveis são estáveis, mas por outro lado são como qualquer outra variável que armazena dados no heap e podem ser usadas como tal.
Dito isto, atualmente existe uma limitação no uso de HashMaps como variáveis estáveis, então tenho que recorrer a arrays. Aqui está um exemplo:

Espero que uma versão futura do SDK DFINITY remova essa limitação para que eu possa simplesmente usar um var player estável sem quaisquer conversões.
Autenticação de usuário
Cada contêiner e cada cliente (por exemplo, linha de comando dfx ou navegador) obterá um ID principal que os identifica exclusivamente (para clientes, tais IDs são gerados automaticamente a partir de pares de chaves públicas/privadas, e a biblioteca DFINITY JS os gerencia e atualmente reside no navegador armazenamento local).
Motoko permite que o contêiner identifique o chamador da função “compartilhada”, que podemos usar para fins de autenticação.
Por exemplo, defino as funções de registro e visualização da seguinte forma:

A expressão msg.caller fornece o ID principal do chamador da mensagem. Observe que isso é diferente do chamador da função.
No Motoko, as mensagens aos atores devem ser enviadas para uma função acessível ao público, que deve ter um tipo de retorno assíncrono.
O código acima mostra duas funções públicas: registrar e visualizar, onde a última é uma chamada de consulta, marcada pela palavra-chave query.
Como vimos, o acesso ao campo chamador da mensagem deve utilizar uma sintaxe especial: shared(msg) ou shared query(msg), onde msg é um parâmetro formal que se refere à mensagem recebida como um todo.
Atualmente, o único atributo que possui é o chamador.
Ser capaz de acessar o ID exclusivo do chamador (remetente da mensagem) parece familiar, como um cookie HTTP.
Mas, diferentemente do HTTP, o Internet Computer Protocol na verdade garante que o ID do sujeito seja criptograficamente seguro e que os programas do usuário executados em computadores da Internet possam ter total confiança em sua autenticidade.
Pessoalmente, acho que fazer com que o programa conheça seu chamador é provavelmente muito poderoso e rígido (por exemplo, o que acontece quando esse ID precisa ser alterado?).
Mas, por enquanto, isso leva a um esquema de autenticação muito simples do qual os desenvolvedores de aplicativos podem aproveitar, e espero ver mais desenvolvimento nesta área.
Simultaneidade e atomicidade
Os clientes do jogo podem enviar mensagens ao servidor do jogo a qualquer momento, portanto, é responsabilidade do servidor lidar corretamente com as solicitações simultâneas.
Em uma arquitetura regular, eu teria que construir alguma lógica para determinar a ordem em que os jogadores se movem (geralmente por meio de uma fila de passagem de mensagens ou de um mutex).
Através do modelo de programação de atores utilizado pelo container, esse problema é resolvido automaticamente sem que eu precise escrever nenhum código para ele.
As mensagens são apenas chamadas de função remota e é garantido que o contêiner processe apenas uma mensagem por vez. Isso resulta em uma lógica de programação simplificada e não preciso me preocupar com funções sendo chamadas simultaneamente.
Como o estado do contêiner só é retido depois que uma mensagem foi completamente processada (ou seja, o retorno da chamada de função pública), não preciso me preocupar em liberar memória para o disco, se as exceções causam corrupção no estado do disco ou estão relacionadas à confiabilidade.
Observe também que as alterações de estado persistentes são atômicas por mensagem.
As funções públicas são livres para chamar qualquer outra função não assíncrona, e o estado alterado é retido desde que toda a execução seja concluída sem erros (para chamadas de atualização, mais detalhes abaixo).
Uma granularidade mais fina pode ser alcançada através da emissão de chamadas assíncronas em vez de chamadas síncronas, que se tornam novas mensagens para o sistema agendar em vez de executar imediatamente.
Se eu fosse construir este jogo usando uma arquitetura convencional, provavelmente escolheria também uma estrutura de ator, como Akka de Java, Actix de Rust, etc.
Motoko oferece suporte a atores nativos, juntando-se à família de linguagens de programação baseadas em atores, como Erlang e Pony.
Atualizar chamadas e consultar chamadas
Acho que esse recurso poderia realmente melhorar a experiência do usuário para aplicativos de computador na Internet e os colocaria no mesmo nível do que as plataformas de nuvem tradicionais hospedam (e muito mais rápido em comparação com outros blockchains).
É também um conceito simples: qualquer função pública que não exija alteração do estado do programa pode ser marcada como uma chamada de “consulta”, caso contrário, será tratada como uma chamada de “atualização” por padrão.
A diferença entre consultas e atualizações é latência e simultaneidade:
Uma chamada de consulta pode levar apenas alguns milissegundos para ser concluída, enquanto uma chamada de atualização leva cerca de dois segundos.
As chamadas de consulta podem ser executadas simultaneamente e bem dimensionadas, as chamadas de atualização são feitas sequencialmente (com base no modelo de ator) e fornecem garantias de atomicidade.
Assim como no exemplo de código acima, consegui marcar a função de visualização como uma chamada de consulta porque ela simplesmente consulta e retorna o estado do jogo que o jogador está jogando.
Na verdade, na maioria das vezes que navegamos na web, fazemos chamadas de consulta: os dados são recuperados do servidor, mas não modificados.
Por outro lado, a função de registro acima é mantida como uma chamada de atualização, pois deve adicionar o novo jogador à lista de jogadores após o registro bem-sucedido.
As chamadas de atualização levarão mais tempo por vários motivos, como consistência de dados, atomicidade e confiabilidade.
Mas este não é um problema inerente aos computadores da Internet.
Muitas ações na web hoje em dia levam mais de dois segundos para serem concluídas, como pagar com cartão de crédito, fazer um pedido ou fazer login em uma conta bancária, para citar alguns.
Acho que dois segundos é o ponto crítico para uma boa experiência do usuário.
Voltando ao jogo reverso, quando o jogador fizer seu próximo movimento, também deverá ser uma chamada de atualização:

Se um jogo atualizar sua tela apenas dois segundos depois que um jogador clicar com o mouse (ou tocar na tela), ele não responderá e ninguém vai querer jogar com um timing tão ruim.
Portanto, tive que otimizar essa parte reagindo à entrada do usuário diretamente no lado do cliente, sem ter que esperar a resposta do servidor.
Isso significa que a interface do usuário front-end terá que validar os movimentos do jogador, calcular quais peças serão viradas e exibi-las na tela imediatamente.
Isso também significa que o que quer que o frontend mostre ao jogador, quando ele voltar, terá que corresponder à resposta do servidor à mesma ação, caso contrário corremos o risco de encontrar inconsistências.
Mas, novamente, acredito que qualquer implementação razoável de um jogo multiplayer bidirecional ou de xadrez pode fazer isso, independentemente de seu back-end levar 200 ms para responder ou 2 segundos.
cliente front-end
O SDK DFINITY fornece um front-end que carrega aplicativos diretamente no navegador.
No entanto, é diferente das páginas HTML comuns servidas por servidores web.
A comunicação com o contêiner de back-end é feita por meio de chamadas de função remotas, que no caso de navegadores são sobrepostas ao HTTP.
Isso é tratado de forma transparente pela biblioteca do usuário JS, portanto, um programa JS simplesmente importa o contêiner como um objeto JS e pode chamar suas funções públicas da mesma forma que as funções JS assíncronas regulares do objeto.
O SDK DFINITY possui um conjunto de tutoriais sobre como configurar um front end JS, então não entrarei em detalhes aqui.
Nos bastidores, o comando dfx no SDK usa Webpack para agrupar recursos, incluindo JS, CSS, imagens e outros arquivos que você possa ter.
Você também pode combinar suas estruturas JS favoritas (como React, AngularJS, Vue.js, etc.) com a biblioteca de usuário DFINITY para desenvolver um front-end JS para uso em navegadores ou aplicativos móveis.
Principais componentes da IU
Sou relativamente novo no desenvolvimento front-end e tenho apenas uma breve experiência com React.
Tomei a liberdade de aprender Mithril desta vez porque tinha ouvido muitas coisas boas sobre Mithril, especialmente sua simplicidade.
Para simplificar, também criei um design com apenas duas telas:
Uma tela “Jogar” que permite aos jogadores inserir seu próprio nome e o nome do oponente antes de entrar na tela “Jogo”. Ele também exibirá algumas dicas e instruções, um gráfico dos melhores jogadores, jogadores recentes e muito mais.
Uma tela de “jogo” que aceita a entrada do jogador e se comunica com o contêiner de back-end para renderizar um tabuleiro inverso. Também exibirá a pontuação do jogador no final do jogo e depois levará o jogador de volta à tela do Jogo.
O trecho de código a seguir mostra a estrutura do front-end do jogo JS:

Existem algumas coisas a serem observadas:
Assim como qualquer outra biblioteca JS, o reversi do contêiner de back-end principal é importado. Pense nele como um proxy que encaminha chamadas de função para um servidor remoto, recebe respostas e lida de forma transparente com a autenticação necessária, assinatura de mensagens, serialização/desserialização de dados, propagação de erros, etc.
Outro contêiner reversi_assets também será importado. Esta é uma forma de obter os ativos necessários agrupados com o Webpack ao instalar o contêiner de back-end. Neste caso tenho um arquivo de som que será reproduzido quando o jogador colocar uma nova peça.
Uma imagem de logotipo que vai direto para ela. Isso deve ser configurado no Webpack usando url-loader, que é uma ferramenta que realmente incorpora o conteúdo da imagem como uma string Base64 a ser usada para o elemento de imagem. Funciona para imagens pequenas, mas não para imagens grandes.
A aplicação final é configurada usando Mithril através dos dois caminhos /play e /game. Este último assume os nomes do jogador e do adversário como dois parâmetros, o que permite recarregar a tela do jogo no navegador sem interromper o jogo.
Carregar recursos do contêiner de ativos
Como sou novo no carregamento de elementos DOM de forma assíncrona em JS, me esforcei bastante para fazer isso.
Quando o DFX constrói o jar, ele também cria um jar reversi_assets, que basicamente empacota tudo em src/reversi_assets/assets/ nele.
Eu uso isso para recuperar um arquivo de som, mas carregá-lo corretamente não é tão simples quanto colocar a URL do arquivo mp3 no campo src do elemento HTML.
Veja como eu carrego (se você é um desenvolvedor front-end, provavelmente já sabe disso):

Quando a função start é chamada (do contexto assíncrono), ela tentará recuperar o arquivo "put.mp3" do contêiner remoto.
Após a recuperação bem-sucedida, ele usa a ferramenta JS AudioContext para decodificar os dados de áudio e inicializar a variável global putsound.
Se putsound for inicializado corretamente, uma chamada para playAudio(putsound) reproduzirá o som real:

Outros recursos podem ser carregados de maneira semelhante. Não estou usando nenhuma imagem além do logotipo, que é pequeno e pode ter seu código-fonte incorporado ao Webpack adicionando a seguinte configuração ao webpack.config.js:

formato de troca de dados
O conceito de Motoko são dados “compartilháveis”, ou seja, dados que podem ser enviados através de contêineres ou limites de idioma.
Obviamente, eu não imaginaria que um ponteiro de heap em C fosse "compartilhável", mas para mim qualquer coisa que possa ser mapeada para JSON é "compartilhável".
Para tanto, a DFINITY desenvolveu uma IDL (Interface Description Language) chamada Candid para aplicações de computador na Internet.
O Candid simplifica muito a maneira como o front-end se comunica com o back-end ou entre contêineres.
Por exemplo, aqui está um trecho (incompleto) do contêiner reversível de back-end descrito por Candid:

Tomemos o método move como exemplo:
Este é um dos métodos exportados na interface de serviço do contêiner.
Recebe como entrada dois inteiros (representando uma coordenada) e retorna um resultado do tipo MoveResult.
MoveResult é uma variante (também conhecida como enumeração) que representa os resultados e erros que podem ocorrer quando o jogador se move.
Nos vários ramos do MoveResult, GameOver indica que o jogo está completo e assume um parâmetro ColorCount, que representa o número de peças pretas e brancas no tabuleiro de jogo.
O código-fonte do Motoko gera automaticamente um arquivo Candid para cada contêiner e é usado automaticamente pela biblioteca do usuário JS sem envolvimento do desenvolvedor:
Do lado do Motoko, cada tipo Candid corresponde a um tipo Motoko e cada método corresponde a uma função pública.
No lado JS, cada tipo Candid corresponde a um objeto JSON e cada método corresponde a uma função membro do objeto contêiner importado.
A maioria dos tipos Candid tem representação JS direta, alguns requerem alguma conversão.
Por exemplo, nat é uma precisão arbitrária em Motoko e Candid, em JS é mapeado para o número inteiro bignumber.js, portanto deve ser convertido para o tipo de número nativo JS usando n.toNumber().
Um problema que tenho é com valores nulos no Candid (e no tipo Option da Motoko).
É representado em JSON como um array vazio[], em vez de seu nulo nativo. Isto serve para distinguir casos onde temos opções aninhadas, como Option >:

Candid é muito poderoso, embora superficialmente pareça muito com Protocolbuf ou JSON.
Então, por que é necessário?
Existem muitos bons motivos além do que é apresentado aqui, e encorajo qualquer pessoa interessada neste tópico a ler o Candid Spec.
Sincronize o estado do jogo com o back-end
Como mencionado antes, usei um truque para reagir imediatamente à entrada válida do usuário, sem ter que esperar a resposta do servidor back-end do jogo.
Isso significa que o frontend só precisa do reconhecimento do servidor do jogo (ou, se houver, do tratamento de erros) após o jogador se mover.
Além de enviar seus próprios movimentos, o cliente também deve conhecer os movimentos do outro jogador.
Isso é feito chamando periodicamente a função view() do contêiner do jogo hospedado no servidor.
A implicação desse design é que tenho que repetir parte da mesma lógica de jogo no backend (Motoko) e no frontend (JS), o que não é o ideal.
Como o Motoko compila no Wasm, e o Wasm é executado no navegador, não seria ótimo se o front-end e o back-end pudessem compartilhar o mesmo módulo Wasm que implementa a lógica principal do jogo? Esse tipo de compartilhamento compartilha apenas código, não estado.
Pode exigir alguma configuração, mas acho que é perfeitamente possível e posso tentar em uma atualização futura.
Especialmente no jogo reverso, em alguns casos um jogador pode ser impedido de realizar qualquer ação, de modo que o outro jogador pode realizar duas ações consecutivas ou até mais.
Para exibir cada movimento feito pelo jogador, optei por implementar o estado do jogo como uma sequência de ações, em vez de apenas o estado mais recente do tabuleiro.
Isso também significa que comparando a lista de ações no estado local do frontend com o que é retornado ao chamar a função view(), podemos facilmente saber o que aconteceu desde a última ação do jogador (é a vez do jogador dar o próximo passo). , etc.
Animação SVG
O assunto da animação usando Scalable Vector Graphics (SVG) pode não pertencer a este artigo, mas uma vez eu realmente fiquei preso nele.
Então, quero compartilhar as lições que aprendi.
O problema que estou tendo é que quando uso repeatCount para definir a animação para ser exibida apenas uma vez, a animação não inicia.
A maioria dos recursos online em SVG fornece apenas exemplos que podem ser repetidos infinitamente ou com uma configuração de repetiçãoCount.
Eles assumem implicitamente que se a animação for mostrada apenas uma vez, ela começará após o carregamento da página (ou algum atraso for definido).
No entanto, com a maioria das estruturas de aplicativos de uma página, como React ou Mithril, a página geralmente não é recarregada, mas simplesmente renderizada novamente.
Então, quando eu quero mostrar um fragmento de jogo mudando de branco para preto ou de preto para branco, isso tem que acontecer quando a página é renderizada novamente, não quando a página é recarregada.
Perdi essa diferença fundamental e só a descobri depois de tentar várias vezes.
Então é assim que uso o Mithril para renderizar um elemento animado (como filho de um SVG) onde o rx da elipse muda do raio inicial para 0 e vice-versa.

A explicação é a seguinte:
start está definido como indeterminado para que a animação possa ser controlada/iniciada manualmente
O preenchimento está definido para congelar, o que significa que após o término da animação, seu estado final permanecerá inalterado.
Os valores são definidos como 4 valores onde os dois primeiros são repetidos como um truque para iniciar a animação após um atraso de 0,1s (1/4 de dur), isso ocorre porque o início está definido como indefinido
O ponto principal é que a animação deve ser iniciada manualmente. Eu o aciono com um atraso de 0s usando setTimeout, um truque que espera até que o novo elemento da UI preparado por Mithril seja renderizado no DOM do navegador:

Conforme mencionado acima, qualquer elemento animado cujo ID não comece com “ponto” será iniciado imediatamente.
Processo de desenvolvimento
Desenvolvi o jogo em Linux e a configuração inicial consistiu em instalar o SDK DFINITY e seguir suas instruções para criar o projeto.
Lembrar de todas as linhas de comando do dfx é complicado, então criei um Makefile para ajudar.

A depuração e o teste são feitos principalmente no navegador, portanto, é necessário muito console.log().
Na verdade, existe uma maneira de escrever testes unitários no Motoko, mas só aprendi sobre isso depois de escrever um jogo.
Inicialmente também desenvolvi um frontend baseado em terminal usando shell scripts e dfx.
Acho que isso ajuda a acelerar a depuração sem precisar passar pelo navegador.
Mas é claro que o teste unitário é a melhor maneira de garantir a correção.
jogar jogos!
Para realmente rodar este jogo em um computador com Internet, existe agora uma rede Tungsten aberta a desenvolvedores terceirizados.
Eu encorajo você a se inscrever, clonar este projeto e implantar o jogo você mesmo para obter experiência de desenvolvedor em primeira mão.
Mas os não desenvolvedores não podem acessar o aplicativo no Tungsten porque ele ainda não é público.
Então, eu também o hospedei usando dfx e nginx como proxy reverso para poder convidar amigos para jogar.
Eu não encorajaria as pessoas a fazerem isso sozinhas, pois o software ainda está em fase Alpha.
Este é um link para o jogo real, apenas para fins de demonstração. Meu plano é implantá-lo em uma rede pública de computadores na Internet assim que for lançado ainda este ano.
Se você tiver alguma dúvida, fique à vontade para visitar o repositório do projeto e enviar um problema, solicitações pull também são bem-vindas!
Junte-se à nossa comunidade de desenvolvedores e comece a construir em forum.dfinity.org.

Conteúdo IC que lhe interessa
Progresso Tecnológico | Informações do Projeto |
Colete e siga o canal IC Binance
Mantenha-se atualizado com as informações mais recentes
