quarta-feira, 26 de janeiro de 2022

Conway's Game of life - Parte III

Nos posts anteriores, implementamos o jogo da vida (Conway's Game of life). Você pode ler aqui a Parte I e a Parte II. Um exemplo de reprodução do jogo é mostrado abaixo.


O problema que tínhamos na nossa implementação é ter que rodar a função para cada passo do algoritmo. Como discutido anteriormente, se queremos avançar n passos, poderíamos usar uma estrutura de repetição que executasse nosso algoritmo n vezes. Qual o problema dessa ideia? Devemos saber exatamente quantos passos queremos dar no jogo. Além disso, uma das maravilhas da simulação do jogo da vida é exatamente visualizar as coisas acontecendo! Para não perdermos esse brilho, vamos implementar uma estrutura de repetição em que apertamos Enter cada vez que queremos avançar no algoritmo e digitamos 'p' para parar.

Vejamos como fica nosso código:

para_alg = F;
while(para_alg==F){
  estado_atual = novo_estado;
  vizinhos = calcula_vizinhos(estado_atual);
  novo_estado = proximo_estado(estado_atual,vizinhos);
  mostra_pop(novo_estado);
  
  varc = readline(prompt = "\n Digite 'p' para parar ou Enter para continuar: ");
  if(varc=='p') para_alg=T;
}

Vou explicar o que está acontecendo. Declaramos uma variável flag, que irá indicar quando o comando while deve parar de executar. Enquanto essa variável tiver valor False, nosso bloco de código será executado.

As primeiras três linhas dentro do comando while nós já vimos. De fato, foram implementadas no post anterior. A função mostra_pop é uma função para criar os gráficos do jogo, como os três primeiros gráficos desse post. Deixarei essa função no final desse post, mas desafio você a tentar escrevê-la sem minha ajuda. Será um bom treino.

As próximas duas linhas de código são a parte que queríamos: pergunta ao usuário se quer continuar ou parar. Caso o usuário queira parar, digita 'p' e o comando while para de ser executado. Dessa forma, podemos simplesmente pressionar Enter para continuar a execução do nosso jogo até ficarmos satisfeitos e querermos pará-lo. Note que, na verdade, qualquer texto que digitarmos que não seja 'p' fará o algoritmo dar mais um passo (mas você pode tratar isso, deixo como um treino).

Podemos melhorar isso? Sim. Eu disse no começo do post que é fácil implementar essa dinâmica para n passos, porém perderíamos o brilho de ver o algoritmo executando, já que os comandos são executados muito rapidamente. Porém, podemos utilizar uma função que faz com que o R espere um tempo antes de executar o próximo bloco de código (que, no nosso caso, seria fazer outra iteração). Utilizaremos a função Sys.sleep(), em que passamos como argumento o tempo (em segundos) em que o R ficará em suspensão. Vejamos como fica nosso código:


n=10;
for(i in 1:n){
  estado_atual = novo_estado;
  vizinhos = calcula_vizinhos(estado_atual)
  novo_estado = proximo_estado(estado_atual,vizinhos);
  mostra_pop(novo_estado)
  
  Sys.sleep(1);
}

Nesse caso, veremos visualmente o algoritmo executar os 10 passos, tomando 1 segundo a cada passo. É o suficiente para vermos de forma clara a simulação do jogo acontecer. O problema dessa implementação é que, se quisermos parar em um determinado passo, não conseguimos. Paramos apenas após os n passos que definimos inicialmente.

Agora sim, temos maneiras fáceis de visualizar a simulação do jogo da vida.

Falta mostrar o código da função mostra_pop. E aí, você conseguiu construir o gráfico do jogo? Vai aí a minha resolução:

mostra_pop = function(estado,titulo=NULL){
  nrow_ = nrow(estado);
  ncol_ = ncol(estado);
  
  plot(0,0,type='n',ylim=c(nrow_+0.5,0.5),xlim=c(0.5,ncol_+0.5),
       xlab='',ylab='',main=titulo,xaxs='i',yaxs='i');
  abline(v=seq(-0.5,(ncol_+0.5),by=1),
         h=seq(-0.5,(nrow_+0.5),by=1));
  
  id_1 = which(estado==1);
  linha = id_1%%nrow_;
  coluna = ceiling(id_1/nrow_);
  
  rect(coluna-0.5,linha-0.5,coluna+0.5,linha+0.5,col=1);
}

Basicamente o código se baseia nas funções plot e rect. Como já falamos delas por aqui, não há necessidade de explicá-las. Vou focar nas seguintes três linhas de código:


  id_1 = which(estado==1);
  linha = id_1%%nrow_;
  coluna = ceiling(id_1/nrow_);

A primeira linha vai identificar as células que estão vivas na gride. O cuidado aqui é que estado se refere a uma matriz e a função which irá retornar os índices como se a matriz fosse um vetor. Por exemplo, em uma matriz 2x2, temos quatro células, e a função which irá retornar os índices (se existirem na condição) de 1 a 4. Dessa forma, precisamos identificar na matriz quais são esses índices.

Para isso, temos que saber como a função which identifica tais índices. Ela faz isso 'transformando' a matriz em um vetor, em que as colunas são concatenadas. No exemplo da matriz 2x2, o vetor ficaria da seguinte forma: ([1,1],[2,1],[1,2],[2,2]) que corresponde aos índices (1,2,3,4). 

Sabendo disso, para identificar a que linha determinado índice se refere, basta tomarmos o resto da divisão desse índice pelo número de linhas da matriz. Por exemplo, o índice 3 (que é a célula [1,2]) se refere a linha 1, que é o resto da divisão de 3 por 2.

Para identificar a coluna, basta arredondar para cima a divisão do índice pelo número de linhas. No caso, 3 dividido por 2 é 1,5 e, arredondando para cima, identificamos a segunda coluna. A ideia é como se tivéssemos percorrido uma coluna e meia, o que indica que estamos parados na segunda coluna. Dessa forma, identificamos, na matriz, quais células estão vivas e desenhamos o retângulo preenchido no gráfico.

Espero que tenham gostado.
Até a próxima aula!

Nenhum comentário:

Postar um comentário