Gameplay comentado de Replicube: resolvendo 18 quebra-cabeças que desenham sólidos de voxels com código Lua

Traduzido de 日本語

Abrir no YouTube ↗
Visão geral

Replicube é um jogo de quebra-cabeça dividido em fases, em que o objetivo é reproduzir um objeto 3D determinado. Em vez de posicionar os voxels à mão, você escreve um programa para posicioná-los. Neste vídeo, quem faz o gameplay resolve as 11 questões do primeiro conjunto de tutorial e as 7 questões do conjunto adicional que chega em seguida. A proposta não é zerar todas as fases, mas encerrar depois de terminar os dois conjuntos de questões. Ao longo de todo o vídeo, a prioridade é escrever código legível, e não otimizar a pontuação.

15 min de leitura
0:23

Como o jogo funciona: receber coordenadas e devolver uma cor

Segundo o tutorial, o jogo consiste em posicionar voxels de 16 cores num espaço de voxels parecido com o de Minecraft. Voxel é uma palavra criada com o sentido de "pixel com volume". O posicionamento é feito com código: se você escreve um código que sempre devolve branco, todo o espaço fica preenchido de branco; se devolve roxo, fica roxo; se devolve azul, fica azul. Ou seja, o formato é escrever um código que recebe as coordenadas de cada voxel e devolve o valor da cor a ser colocada ali. Dizendo "Já estou começando a ficar com dor de cabeça", a pessoa comenta que ultimamente só tem jogado jogos de luta, então de vez em quando é bom jogar algo tranquilo assim.

No jogo, chega um e-mail do administrador de um lugar chamado Cubenet, e o fluxo é resolver o conjunto de questões básicas anexado a ele. Na parte de cima da tela aparece o formato correto, e na de baixo, o formato desenhado pelo código atual. A avaliação parece incluir também a quantidade de código e a velocidade de execução, mas quem joga deixa a otimização como "sobremesa para depois" e decide priorizar primeiro a legibilidade. Como justificativa, afirma que "código, basicamente, é escrito para humanos".

1:47

1-1 a 1-3: trocar cores e estruturas condicionais

Na 1ª questão, lendo os comentários do código já existente, dá para perceber que só a cor devolvida está um pouco diferente. A fase foi resolvida trocando PURPLE por RED.

Na 2ª questão, é preciso pintar com duas cores conforme a posição. Como são dadas as variáveis x, y e z que representam as coordenadas, a solução foi escrever um if que devolve DARKBLUE se y for maior ou igual a 1 e ORANGE caso contrário.

O tema da 3ª questão são os operadores de comparação. Quem joga repara que, nessa linguagem, a desigualdade se escreve "~=" e prevê que vai acabar usando isso em breve. O problema em si segue praticamente o mesmo padrão da 2ª questão e pode ser resolvido com uma condição do tipo "y maior ou igual a tal valor". Foi citada uma solução alternativa sem operadores de comparação, como return GREEN - (y + 4) // 3 * 2, mas ela não foi adotada porque esse tipo de código tende a confundir quem lê.

3:55

1-4 e 1-5: como deixar transparente

A partir da 4ª questão, além da coordenada y, passam a ser usadas também as coordenadas x e z. O código inicial já traz um esquema de cores de arco-íris. Como ele reaproveita diretamente o valor da coordenada y como código de cor, a cada camada que sobe a cor muda para a seguinte. Mas o que esta questão exige é apenas deixar uma parte do objeto transparente. Segundo os comentários, basta não devolver nada ou devolver 0 ou EMPTY para que fique transparente. A fase foi resolvida trocando a condição "quando x e z forem maiores ou iguais a 2" do código inicial por "quando forem maiores ou iguais a 0".

A 5ª questão tem praticamente a mesma estrutura e foi resolvida com a condição "quando x, y e z forem todos maiores ou iguais a 1".

5:42

1-6: desenhar formas simétricas com valor absoluto

Na 6ª questão aparece a função abs. O comentário diz que é uma função que devolve o valor absoluto e que é muito útil para desenhar objetos com simetria axial. O formato-alvo desta vez é simétrico em relação a todos os eixos.

Primeiro, quem joga foca no eixo z e define a espessura fazendo o código devolver vazio se o valor absoluto de z for maior ou igual a 2. Também daria para escrever "maior ou igual a 2 ou menor ou igual a -2", mas a avaliação é que usar o conceito de valor absoluto deixa o código mais fácil de entender. O restante, em forma de jogo da velha, também foi feito com valor absoluto, criando as barras verticais e horizontais e unindo-as com o "ou" lógico (or). Como estruturas simétricas devem aparecer com frequência daqui em diante, a expectativa é que o valor absoluto seja bastante usado.

6:49

1-7: ler a mensagem de erro e corrigir

A 7ª questão já começa com um erro. Primeiro, a mensagem de erro é lida. Pela indicação "LUA_ERRSYNTAX", deduz-se que a linguagem é Lua. Era esperado um end para fechar o if da linha 10, mas há um if escrito na linha 13. Ou seja, faltou escrever o end.

Corrigir isso não basta: ainda restava um trecho em que uma caixa que deveria ser vermelha estava GREY, e a fase foi resolvida reescrevendo isso também. Aqui, comenta-se que as oportunidades de escrever código do zero são, na verdade, poucas, e que na prática a capacidade de ler o código dos outros costuma ser mais importante.

7:45

1-8: o desafio livre "sushi?" resolvido com distância de tabuleiro de xadrez e tabela

Só que a 8ª questão pedia justamente para escrever do zero. O nome da questão é "sushi?", e quem joga sente ali uma "sensibilidade absurda". Como parecia ser permitido criar a forma livremente, a estratégia foi pintar de vermelho, branco e verde de acordo com a distância em relação ao centro.

A distância usada aqui é a chamada distância de tabuleiro de xadrez. É a distância medida em quantos passos uma peça que pode se mover em 8 direções leva para chegar a um ponto, e o conceito é apresentado como algo que de vez em quando ajuda em roguelikes como Shiren the Wanderer. Como essa distância é fácil de calcular quando há uma função max, a aba Documentation foi aberta para pesquisar. A documentação tem 4 capítulos: o 1º é uma visão geral, o 2º traz detalhes sobre o desenho, e o 3º parecia ter a informação procurada. Ali, confirmou-se que é possível definir variáveis, que operadores como + e - podem ser usados e que existe uma função max.

Quanto à conversão para vermelho, branco e verde, a ideia era, se não houvesse nenhum recurso conveniente, ramificar de forma direta com ifs. Como proposta de "implementação divertida", também foi citado o método de enfileirar 7, 1 e 11 em hexadecimal, formando 0xb17, e deslocá-lo para a direita de acordo com a distância.

Aqui, como digressão, a pessoa conta que os plugins do OBS, que usa normalmente para gravar os vídeos, também podem ser escritos em Lua. Já escreveu um plugin simples que gera automaticamente as marcações de tempo do YouTube, e talvez essa tenha sido a única vez na vida em que mexeu com Lua.

Na implementação, primeiro foi calculada a distância de tabuleiro de xadrez a partir da origem, tomando o máximo entre o valor absoluto de x e o valor absoluto de y. Desta vez o resultado seria 0, 1 ou 2, então era preciso um mecanismo para convertê-lo em 0→7 (vermelho), 1→1 (branco) e 2→11 (verde). Como a palavra "table" apareceu de relance na documentação, deduziu-se que daria para usar uma estrutura parecida com um array. A tentativa de escrever um literal de array com colchetes, que funciona em muitas linguagens, falhou em Lua. Então, imaginando pelo nome "Table" que seria no estilo de chaves, o código foi reescrito e funcionou quase perfeitamente. Mas as cores estavam deslocadas em uma posição, revelando que os índices em Lua começam em 1. Depois de ajustar isso, o sushi ficou pronto.

Os elementos usados eram um tanto complicados, mas, olhando o código, ele parece surpreendentemente conciso, diz quem joga. Porém, acrescenta, quem pensa assim costuma ser apenas quem escreveu o código.

11:25

1-9 e 1-10: "onde não pintar" e ifs aninhados

A 9ª questão é quase um tutorial de controles, sobre mover o ponto de vista com o mouse para conferir bem a forma. É uma caixa aberta em cima, e basta devolver BROWN quando x ou z for ±3, ou quando y for -3. Excluindo o caso de y ser +3, obtém-se a forma com a tampa de cima aberta. Aqui também foi usado o valor absoluto. Os comentários pareciam querer que a solução fosse expressa em termos de "onde não pintar" em vez de "onde pintar", mas a avaliação foi de que tanto faz. Como tinha caprichado demais no sushi anterior, a fase pareceu um pouco decepcionante.

A 10ª questão tem, segundo quem joga, um esquema de cores parecido com o do Google Chrome, mas não se vê nenhuma regularidade marcante na disposição das cores. Os comentários também pareciam esperar ifs aninhados, então foi escrita uma estrutura condicional direta. Quando y é maior ou igual a 1, devolve GREEN se x for maior ou igual a 1, e ORANGE caso contrário. Quando y não é maior ou igual a 1, devolve BLUE se z for maior ou igual a 1, e RED caso contrário. O código foi escrito girando o ponto de vista para enxergar melhor os eixos.

13:34

1-11: dois erros de digitação e o mecanismo do X

A 11ª questão, a última do tutorial, também já começa com problemas. Segundo a mensagem de erro, a linha 31 usa uma variável desconhecida chamada "colo". Deduzindo que era um erro de digitação de "color", isso foi corrigido. Mas só isso não bastou: a escada era desenhada apenas em parte. Lendo mais o código, havia um trecho com a grafia do inglês britânico, "colour", e corrigir isso completou a fase.

Sobre por que esse código consegue desenhar um X, a interpretação foi de que é porque ele usa as expressões x==y e x==-y. Ou seja, usando funções do primeiro grau como igualdades, é possível desenhar esse tipo de forma. Neste ponto, a pessoa diz: "Bom, deve ser um conhecimento que não tem nada a ver comigo".

14:40

2-1 a 2-3: desenhar linhas diagonais com igualdades

Ao terminar o tutorial, chegam um e-mail de boas-vindas da comunidade e um conjunto adicional de 7 questões.

Logo na 1ª questão do conjunto adicional, aparece um problema de função do primeiro grau, justamente aquilo que tinha acabado de ser chamado de "nada a ver comigo". Como é a reta y = -x, em Lua pode-se escrever x == -y. A comparação com == devolve true ou false, mas no Replicube isso é desenhado como branco e vazio, então bastou devolver diretamente o resultado da comparação para resolver a fase.

A 2ª questão também é uma reta diagonal, mas é preciso restringir ainda mais pela coordenada z. Primeiro foi desenhado um plano com x == y, e depois a condição x == z foi unida com o "e" lógico (and) para completar a forma. Para que o valor devolvido fosse GREEN ou vazio em vez de true/false, foi acrescentado and GREEN no final, aproveitando a avaliação de curto-circuito.

A 3ª questão tem a forma da 2ª espelhada, então bastou trocar a expressão por uma versão com valor absoluto. Calcular abs(x) duas vezes é um pouco desperdício, mas como não é uma operação lenta, decidiu-se não se preocupar com isso.

17:00

2-4 a 2-6: criar padrões com resto e soma

A 4ª questão apresenta o operador %, que devolve o resto da divisão. O código inicial usa o resto da divisão de y por 3 para distribuir os valores em sequência entre 0, 1 e 2. Olhando a forma correta, parecia que o valor de x estava sendo convertido em 0 ou 1, então deduziu-se que bastava usar o resto da divisão por 2. Como estava deslocado em uma casa, o ajuste foi subtrair 1 de x, e a fase foi resolvida.

A 5ª questão é um tabuleiro de damas. Primeiro, para deixar tudo num plano, a ideia era escrever algo que devolvesse vazio quando o valor não fosse 0, mas a condição foi escrita inicialmente com z e depois corrigida para y. As setas RGB correspondem a XYZ, mas às vezes isso é esquecido e as coisas se embaralham, comenta a pessoa. Quanto ao padrão, já tinha "dado uma espiada às escondidas" no fato de que, no código inicial da 2ª questão, somar com return x+y produz listras diagonais. Arredondando isso com %, obtém-se o tabuleiro de damas. Na primeira tentativa ficou rosa, então o código foi corrigido para devolver preto, e a fase foi resolvida. Também foi mostrado que, se quiser evitar estruturas condicionais, dá para fazer isso com uma expressão que converte os restos 0 e 1 em 1 e 3, de branco e preto, mas a avaliação é de que a conversão para cores fica muito difícil de entender.

A 6ª questão é um padrão de arco-íris diagonal. Aproveitando o fato de que a soma desenha listras diagonais, primeiro foram somados x e y. Com isso, a aparência ficou mais ou menos parecida. Porém, no código de quem joga a origem devolvia 0, enquanto na resposta correta ela era LIGHTGREEN, então foi somado LIGHTGREEN para acertar. Por fim, a espessura foi ajustada devolvendo vazio quando z fosse diferente de 0, e a fase foi concluída.

19:55

2-7: a mesa de quatro pés e a organização com funções

A última questão é uma típica mesa de quatro pés, e todo o material é BROWN. Como só era preciso pensar na forma, seguindo o processo real de montagem, o tampo e os pés foram tratados separadamente. Ao descobrir que dá para escrever comentários com --, também foram adicionados comentários. O tampo é apenas um plano que devolve BROWN quando y é 1. Para os pés, primeiro foi definida a altura limitando a área de desenho a y menor ou igual a 0. Como as posições são simétricas, como de costume foi usado o valor absoluto, desenhando apenas quando os valores absolutos de x e z são ambos 2. Essas condições foram unidas com and, devolvendo BROWN quando todas são satisfeitas, e a fase foi concluída.

Mas, com um pouco de pena de terminar, a pessoa decidiu mexer mais um pouco. Segundo a documentação, era possível definir funções próprias, e havia vontade de experimentar isso. O código atual escrevia return BROWN duas vezes, embora todo o material fosse BROWN, e se alguém pedisse "Na verdade, pode trocar para WHITE?", seria trabalho dobrado. Então o código foi reescrito com funções que tratam só da forma, e a cor passou a ser definida em um único lugar.

Concretamente, foram definidas a função isTopPlate, que devolve se aquela coordenada é o tampo, e a função isLeg, que devolve se é um pé, e foi escrito "se for tampo ou pé, devolve BROWN". Em Lua, o mais comum para nomes de funções provavelmente é snake_case, mas desta vez, como a largura da tela é extremamente estreita, por precaução foi usado camelCase. Embora chamadas de função tenham um certo custo, a avaliação é de que não há problema, já que desenhar objetos estáticos não exige tanto desempenho. Por outro lado, também é mencionado que, como o jogo parece permitir criar animações, nesses casos talvez seja melhor tomar algum cuidado.

22:52

A avaliação de quem joga

Depois de terminar os dois conjuntos de questões, quem joga avalia Replicube da seguinte forma. No começo dava a impressão de ser difícil de entrar, mas a interface e a introdução são muito cuidadosas, e jogar é muito divertido. É um excelente jogo de quebra-cabeça, com vários pontos de vista e soluções possíveis, e também pode ser aproveitado como porta de entrada para o interesse por programação. A conclusão é que também há ligação com áreas da matemática, e que é uma obra que pode ser aproveitada de várias maneiras.