sábado, 21 de janeiro de 2017

Migrando o servidor de chat para Python 3.6

Na época do lançamento do Python 3.4, eu estava tão contente com a integração do Asyncio que escrevi um servidor de chat aqui. O tempo passou e novas versões do Python foram lançadas. Resolvi então migrar o servidor para Python 3.6.

Uma das grandes mudanças que ocorreram no Python 3.5, foi o suporte a async e await para substituir @asyncio.corroutine e yield from respectivamente. Esta pequena mudança por si só já facilita em muito a leitura do código, que ficou mais leve. Mas uma das principais mudanças do Python 3.6 são as f-strings que facilitam a formação de mensagens.

Primeiro, vamos preparar o ambiente. É preciso instalar o Python 3.6. Se você utiliza Windows, basta baixar o pacote no site da Python.org.

Ubuntu 16.10

Se você utiliza Ubuntu 16.10, ainda precisa baixar os fontes e compilar... mas seguindo a recomendação de amigos do Telegram, resolvi experimentar com o pyenv!

Para instalar no Ubuntu, baixe o install_python360.sh e rode com:
bash install_python360.sh


Como alguns pacotes precisam ser instalados no Ubuntu, ele vai usar sudo. Esteja pronto para digitar a senha. No meu caso, como uso docker (docker run -rm -t -i ubuntu:16.10 /bin/bash), rodei o script como root. Se você instalar no seu usuário, ele vai chamar o sudo quando necessário. Eu gravei um pequeno vídeo do que aconteceu na minha instalação:


Windows

Depois de instalar o Python 3.6.0, instale o websockets com pip3 install websockets

Outros sistemas

Instale o Python 3.6.0 e o módulo websockets.

O novo servidor


Mudando @asyncio.coroutine para async def, o código já fica mais claro. Em uma segunda passagem, eu substitui os yield from por await. Como estamos usando Python 3.6, não custa adaptar as strings para f-strings. E para terminar a migração, configurei o log para que o código não fique cheio de prints! Ficou assim:

Antes de executar, temos que preparar um certificado SSL (no Linux).

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes


O cliente


Hoje não tem como escapar do Javascript. Fiz poucas alterações no código, a maior delas foi simplesmente cosmética e agora o texto rola para baixo automaticamente quando novas mensagens chegam.


Rodando


Imaginando que você esteja no mesmo diretório dos arquivos deste post, vamos criar um servidor web simples com python, claro:
python -m SimpleHTTPServer 8080


Deixe rodando e abra um outro terminal. Vamos executar nosso servidor:
python server.py


E finalmente, abra o browser usando localhost ou seu ip:
http://localhost:8080/cliente.html

Observação: como utilizamos um certificado auto-assinado, precisamos dar permissão ao browser de abrir a página. Como o websocket apenas usa SSL, abra uma outra janela no browser, mas na porta 8765:
https://localhost:8765

Siga o procedimento de seu browser para abriar a página. Normalmente você deve clicar em um botão dizendo que quer continuar acessando a página. Se tudo der certo, você receberá a mensagem: Invalid request. Feche a janela e recarrege o cliente em:
http://localhost:8080/cliente.html

Ele agora deve ter conectado normalmente. Abra outra janela no mesmo endereço.
Digite:
/nome X

e depois envie uma mensagem. Ela deve aparecer na outra janela. Você deve digitar /nome Nome antes de enviar mensagens. Teste com vários clientes, modifique e divirta-se.







terça-feira, 27 de dezembro de 2016

Consultas via Telegram

Seria legal se profissionais de informática dessem consultas como médicos ou advogados, mas algo nos impede de cobrar por tudo e esse desejo ou intenção de compartilhar ideias nos consome.

Eu participo de vários grupos de Telegram, principalmente sobre Python, um deles é o PyCoding e o outro é o pybr. Normalmente eu leio os grupos quando estou usando meu celular, então nem sempre é possível ajudar com as dúvidas, mas vou tentar separar um pouco de tempo para explorar algumas ideias aqui e lá.

Hoje está tão fácil aprender qualquer coisa que tenho notado uma ansiedade cada vez maior de quem começa a programar de aprender tudo. Em um só mês, algumas pessoas querem aprender Python, SciPi, TensorFlow, Android e o que mais der. Um mês é pouco tempo. Pode-se aprender a programar em períodos relativamente pequenos, mas leva tempo para se acostumar com as novas ideias, linguagens e bibliotecas. O Peter Norvig comentou sobre essa ansiedade no Learn Programming in Ten Years.

Cálculo de médias


Vamos ao interesse do post, a tal consulta.

O colega Wesley enviou dois programas, vou começar pelo mais simples. Primeiro vamos desconsiderar os palavrões, nosso colega é jovem.

Uma coisa que gostei muito foi a primeira linha de mensagens. Poucos se preocupam em dizer o que faz o programa, isso é legal! Eu faria apenas uma pequena modificação para que a linha não fosse tão grande.

Como eu cresci nos anos 80, sem letras minúsculas e acentos, é questão de honra corrigir as mensagens.

Nas linhas 7 a 11, os valores das variávies m1, n1, n2, n3 e n4 são solicitados. Como a função input retorna strings, veja que no resto do programa a função float foi utilizada para converter estes valores. Neste caso, o valor convertido deveria ser armazenado diretamente na variável.

Desta forma, simplificamos a linha 12 de forma a facilmente perceber um erro de prioridade de operações. Quando fazemos o cálculo de n1 + n2 + n3 + n4 / 4, sem utilizar parênteses, as operações são realizados por ordem de prioridade, como na matemática. Assim, n4/4 é somado a n1, n2 e n3. Para calcular a média, precisamos de parênteses: (n1 + n2 + n3 + n4) / 4. Agora, a soma das notas é calculada e depois dividida por quatro, como queríamos.

Entre as linhas 15 e 19 acredito que tenha sido apenas um teste. Vou remover para não atrapalhar o entendimento do programa final.

As linhas de 24 a 35 imprimem vários pontos, vou apenas simplificar.

Para terminar, pequenas modificações para usar as f strings do Python 3.6.


Programa com tkinter

O outro programa é uma interface gráfica, usando tkinter.

Como os fontes foram postados no Telegram, muito se perde. De cara há um problema com o import da linha 2. Eu parabenizo o Wesley pela coragem de usar o tkinter. É umas das partes do Python que menos gosto, mas que funciona.


Deve-se evitar os import * no Python, isso polui o namespace e causa problemas chatos de resolver. No caso do tkinter, é um caso a se pensar, mas nunca misturar o * com os imports de classes e funções individuais.

Uma coisa que salta aos olhos, não, não falo do fundo caladryl, mas da repetição da cor em várias partes do código. Vamos criar uma constante para cor de fundo, melhor, vamos retirar as cores e deixar as cores padrão.

Um outro problema é a validação de valores, acrescentei uma função float_ou_zero que retorna zero caso o valor digitado não possa ser convertido para float.

Usar tkinter sem classes é um tanto confuso, eu particularmente não gosto de ter funções com variáveis globais e de ter definições de funções e variáveis misturadas, mas isso é assunto para outro post.


Vejamos como ficou!




Convertendo ints

Outro post interessante foi o de como converter vários ints de uma só vez. O problema inicial era calcular um valor do tipo hh:mm:ss em total de segundos.


O que me chamou atenção foi uma das soluções:

Correta, porém, achei que o foco da solução não era mais o problema inicial, mas fazer em menos linhas. De repente, passa o medo de "Perlizar" o Python.
O problema em si, exige validação dos dados. Este é um detalhe importante que é fácil de ser esquecido. Então, ao invés de fazer com menos linhas, vamos adicionar o mínimo de validação.
Esta solução utiliza o módulo datetime do Python e o tipo time para validar as horas entre 0 e 23, minutos entre 0 e 60 e o mesmo para segundos. Se o usuário entrar um valor errado, terá que redigitar após receber uma mensagem de erro. Embora eu tenha usado a expansão de listas duas vezes em uma só linha (Perlização?), acho que o código ficou relativamente bom.

São detalhes, mas que fazem a diferença em programas maiores. Nem sempre escrever em menos linhas é o mais correto ou deveria ser o foco principal da solução de um problema.

Ainda sobra margem para uma outra solução, onde criamos uma função para converter horas, minutos e segundos para total em segundos.

Além da validação (ainda que mínima), ganhamos a flexibilidade de digitar valores como 10, 10:20 ou 10:20:30. O programa que fizemos pode ser importado por outros programas e suas funções reutilizadas, sem perder a funcionalidade inicial se usado como programa principal.

sábado, 28 de maio de 2016

Uso de UUIDs como chave primária com Django e Postgres

Por default, o Django cria chaves primárias do tipo inteiro (32 bits) quando usado com o banco PostgreSQL. Estes campos são incrementados automaticamente e funcionam perfeitamente bem em ambiente local. Um problema que aparece quando você cria uma API é o fato de seus ids sequenciais e numéricos exporem detalhes de sua base de dados.

Imagine que seu cliente tem o id 1, ele pode imaginar (e com razão) que é seu primeiro cliente. O mesmo pode ser utilizado por concorrentes para saber quantos novos clientes você obteve em determinado período, bastando para isso criar uma nova conta. Isto pode gerar uma vontade incontrolável em algumas pessoas de explorar os valores de suas chaves. Com chaves inteiras, esta tarefa é fácil, basta incrementar o valor da chave de 1 e tentar novamente.

Uma alternativa às chaves inteiras é a utilização de UUID, geradas aleatoriamente, porém maiores (128 bits). Por serem 4 vezes maiores, já podemos imaginar que o espaço em disco ocupado pelas chaves e índices vai também aumentar. Mas e quanto as operações de inserção, busca e atualização de tabelas? Quanto custa substituir as chaves inteiras por UUIDs.

Exemplo de UUID:
cab5ade3-2dc3-4344-b5a6-80df59f91458

Eu resolvi fazer um teste, comparando chaves inteiras, uuids e uma solução mista, onde a uuid é utilizada fora da aplicação, mas mantendo uma chave primária inteira.

Os modelos são bem simples (chaves inteiras, modelo de referência):

class A(models.Model):
    bigtext = models.TextField()
    name = models.CharField(max_length=100)
    counter = models.IntegerField(default=0)


class B(models.Model):
    parent = models.ForeignKey(A, related_name="bs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)


class C(models.Model):
    parent = models.ForeignKey(B, related_name="cs")
    grandparent = models.ForeignKey(A, related_name="cs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)

A, B, C e foram configuradas de forma a estabeler um relacionamento entre elas.

Vamos modificar os modelos para utilizarmos UUIDs:

class A(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    bigtext = models.TextField()
    name = models.CharField(max_length=100)
    counter = models.IntegerField(default=0)


class B(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    parent = models.ForeignKey(A, related_name="bs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)


class C(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    parent = models.ForeignKey(B, related_name="cs")
    grandparent = models.ForeignKey(A, related_name="cs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)


E uma solução híbrida alternativa, com duas chaves, uma uuid e outra inteira:

class A(models.Model):
    surrogate_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    bigtext = models.TextField()
    name = models.CharField(max_length=100)
    counter = models.IntegerField(default=0)


class B(models.Model):
    surrogate_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    parent = models.ForeignKey(A, related_name="bs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)


class C(models.Model):
    surrogate_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    parent = models.ForeignKey(B, related_name="cs")
    grandparent = models.ForeignKey(A, related_name="cs")
    bigtext = models.TextField()
    counter = models.IntegerField(default=0)


O programa completo pode ser baixado aqui: https://github.com/lskbr/keyperftest

Ambiente de testes:
Processador: Intel Core i7 4790K 4 GHz
Disco: Samsung SSD 850 EVO
Ubuntu 16.04 rodando em VM VirtualBox (4 GB, 4 CPUs)
PostgreSQL 9.5.2 rodando via docker
Django 1.9.6
Python 3.5.1
Testes realizados em: 28/05/2016

Rodando o programa com 1000 registros, obtemos os seguintes resultados (todos os tempos em segundos):


Neste primeiro resultado, percebemos o tempo de inserção com chave inteira e uuid não é muito diferente. A solução mista (chave inteira + uuid) leva mais tempo.

Para simular o tempo de acesso aleatório a tabelas com chave inteira ou uuid, vemos que como esperado, as chaves inteiras possuem a melhor performance. A solução híbrida se apresenta como uma alternativa interessante, uma vez que o relacionamento entre as tabelas continua sendo feito por chaves inteiras (a chave surrogate é utilizada apenas para encontrar o registro em C, as ligações entre A e B são feitas com chaves inteiras).



E nesta imagem o resultado das operações de atualização da tabelas. No caso, atualizamos a partir de C, as tabelas A e B. Mais uma vez as chaves inteiras tiveram um melhor desempenho e a combinação de chave inteira com uuid se apresenta como uma meio termo.

Repetindo os testes, mas desta vez para 10.000 registros, as diferenças se tornam mais claras.

No caso da inserção:

Acesso:


Atualização


Embora haja diferença entre os tempos destas operações, os resultados não demonstram uma lentidão excessiva ao aumentarmos o tamanho da chave 4 vezes, de 32 para 128 bits.

Considerando os tempos das operações com chaves inteiras como 100%, temos os seguintes resultados médios:

Inserção Atualização Acesso
integer 100.00% 100.00% 100.00%
uuid 101.64% 108.77% 108.97%
interger + uuid 106.15% 106.63% 111.85%


Utilizar as UUIDs em URLs também aumenta o tamanho das strings, mas acredito ser um preço a se pagar pela comodidade.

Além de esconder a sequência de chaves e não possibilitar a dedução do número de registros de seu banco de dados, UUID possuem as seguintes vantagens:
a) Podem ser geradas em várias máquinas, permitindo que seu banco rode em vários servidores sincronizados.
b) Evitam ataques por dedução das chaves

Desvantagens de UUIDs:
a) Ocupam mais espaço em disco e em memória (128 bits)
b) São um pouco mais lentas para gerar
c) Aumentam o tamanho das URLs
d) São difíceis de memorizar (o que pode dificultar as tarefas de depuração)

Eu decidi utilizar UUID em projetos futuros, uma vez que a segurança das chaves é mais importante para mim que o espaço ocupado por estas e que não há uma degradação importante na velocidade de acesso ao banco. O uso de UUIDs também facilitar a utilização do banco em clusters e até mesmo a geração de chaves offline.


domingo, 22 de maio de 2016

Convertendo um jogo escrito em Basic para Python - Parte III

Nesta terceira parte, temos os seguintes objetivos:


  • Limpar as classes
  • Vários aviões
  • Múltiplos tiros.
  • Generalizar os objetos do jogo em uma super classe
  • Exibir um placar
  • Atribuir teclas para atirar e jogar de novo ou sair


Na versão da Parte II, as classes tem muito código repetido.
Analisando cada uma delas, podemos chegar a conclusão de um comportamento comum quanto
a forma de desenhar e atualizar os objetos. Um método para retirar o objeto do jogo também é utilizado.

Todo objeto do jogo precisa ter acesso ao jogo em sim.
Embora quase não utilizemos cores, eu adicionei a cor como parâmetro, uma vez que pode ser usada para representar múltiplos jogadores ou diferentes tipos de objetos.

Uma das coisas que não ficaram claras na versão anterior é o controle de colisão. Na realidade, o controle de colisão que utilizamos é bem básico e difícil de generalizar. Para permitir o controle da colisão entre objetos, cada objeto terá uma posição x e y na tela, com altura e largura.

Desta forma, dois objetos colidem se o retângulo formado pelos pontos:

(x, y), (x + largura, y), (x + largura, y + altura), (x, y + altura)

De dois objetos tiverem pontos em comum.

Como diferentes tipos de objetos colidem entre si, a propriedade colisão guarda o tipo de objeto.
Desta forma, podemos separar os tiros do avião em si.

Uma variável de DEBUG também foi adicionada, para mostrar o retângulo de colisão caso precisemos fazer ajustes.

class ObjetoDoJogo:
    def __init__(self, jogo, cor):
        self.jogo = jogo
        self.cor = cor
        self.ativo = True
        self.colisao = FUNDO
        self.largura = 0
        self.altura = 0
        self.x = 0
        self.y = 0
        self.debug = DEBUG

Se debug estiver ativo, vamos desenhar uma caixa em volta do objeto.



Desta forma, fica mais fácil detectar erros nos limites e no tamanho do objeto.

    def desenha(self):
        if self.debug:
            set_color(2)
            pyglet.graphics.draw(4, pyglet.gl.GL_LINE_LOOP,
                             ('v2f', (self.x, self.y,
                                      self.x + self.largura, self.y,
                                      self.x + self.largura, self.y + self.altura,
                                      self.x, self.y + self.altura)))

A atualização das propriedades, embora presente em todos os objetos, não tem um comportamento padrão.
Vamos utilizar apenas pass para marcar o lugar.

    def atualiza(self):
        """Calcula nova posição, atualiza valores a cada frame"""
        pass

Aqui um método para dar uma oportunidade ao objeto de tratar a colisão com outro objeto.
Este método será chamado pelo jogo, sempre que dois objetos colidirem.

    def colidiu(self, outro):
        """O que fazer em caso de colisao? Recebe o outro objeto"""
        pass

E este aqui é o teste de colisão em si, bem simples. Ele só funciona com larguras e alturas positivas.
    def colide(self, outro):
        if self.x < outro.x + outro.largura and self.x + self.largura > outro.x and \
           self.y < outro.y + outro.altura and self.altura + self.y > outro.y:
            self.colidiu(outro)
            outro.colidiu(self)

Quando a colisão é detectada, o método colidiu dos dois objetos é chamado.

E finalmente o método para desativar e retirar o objeto do jogo:

    def desativa(self):
        self.ativo = False
        self.jogo.desativa(self)

Vejamos como ficou a classe do Avião:

class Aviao(ObjetoDoJogo):
    def __init__(self, jogo, cor=3):
        super(Aviao, self).__init__(jogo, cor)
        self.velocidade = (random.randint(0, 6) + 4) * RATIO
        self.y = 159 - (random.randint(0, 135) + 11)
        self.x = self.velocidade
        self.altura = 9
        self.largura = 15
        self.colisao = AVIAO
        self.escapou = False

    def desenha(self):
        super().desenha()
        y = self.y + self.altura
        x = self.x
        set_color(self.cor)
        pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                             ('v2f', (x, y,
                                      x, y - 8,
                                      x + 3, y - 2,
                                      x + 12, y - 2,
                                      x + 14, y,
                                      x, y)))

    def atualiza(self):
        self.x += self.velocidade
        if self.x > 265:
            self.escapou = True
            self.desativa()

    def colidiu(self, outro):
        if outro.colisao == TIRO:
            self.jogo.objetos.append(Explosao(self.jogo, self.x, self.y))
            self.desativa()

Nesta versão, o nome das propriedades já foi ajustado. Não utilizo mais altura_do_aviao que se tornou apenas a coordenada y. Na classe Avião, adicionei a propriedade escapou para marcar os aviões que saem da tela sem terem sido destruídos, dos que são atingidos por mísseis. Mais tarde, esta informação será utilizada para contar pontos.

Quando o avião colide com um Tiro, um novo tipo de objeto, Explosao é adicionado ao jogo. Desta forma, teremos um efeito simples quando o avião for atingido.

Uma das vantagens de se trabalhar com objetos é a facilidade de criá-los. No caso, a explosão é apenas outro objeto adicionado ao jogo.

class Explosao(ObjetoDoJogo):
    def __init__(self, jogo, x, y):
        super(Explosao, self).__init__(jogo, cor=0)
        self.estagio = 8
        self.x = x
        self.y = y

    def desenha(self):
        dx = 4 * self.estagio  #random.randint(1, 4)
        dy = random.randint(1, 4) * self.estagio
        dx2 = random.randint(1, 4) * self.estagio
        dy2 = random.randint(1, 4) * self.estagio
        y = self.y
        set_color(7)
        pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                             ('v2f', (self.x, y,
                                      self.x - dx, y + dy,
                                      self.x + dx2, y + dy2)))
        set_color(8)
        pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                             ('v2f', (self.x, y,
                                      self.x -dx/self.estagio , y + dx,
                                      self.x + dx2 * self.estagio, y + dy2)))

    def atualiza(self):
        self.estagio -= 0.4
        if(self.estagio < 0):
            self.desativa()

A ideia da explosão é que triângulos amarelos e vermelhos, de tamanhos aleatórios apareçam na tela.
Como a explosão não se move, a propriedade estagio foi adicionada para controlar seu tempo de vida.
No caso, a cada frame, subtrairemos 0.4 de estagio e quando este for negativo, o objeto será retirado do jogo.

A classe Tiro foi também melhorada:

class Tiro(ObjetoDoJogo):
    def __init__(self, jogo, posicao):
        super(Tiro, self).__init__(jogo, cor=3)
        self.y = 170
        self.x = (posicao + 1) * 70
        self.velocidade = 5 * RATIO
        self.colisao = 2
        self.altura = 5
        self.largura = 2
        self.errou = False

    def desenha(self):
        super().desenha()
        set_color(self.cor)
        y = self.y + self.altura
        pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                             ('v2f', (self.x, y,
                                      self.x, y - 4)))
        v = 4
        a = random.randint(3, 4)
        set_color(7)
        pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                             ('v2f', (self.x, y,
                                      self.x - 1, y + v,
                                      self.x + 1, y + v)))
        set_color(8)
        pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES,
                             ('v2f', (self.x, y,
                                      self.x - 0.5, y + a,
                                      self.x + 0.5, y + a)))

    def atualiza(self):
        self.y -= self.velocidade
        if self.y < 0:
            self.errou = True
            self.desativa()

    def colidiu(self, outro):
        if outro.colisao == AVIAO:
            self.desativa()

O Tiro agora tem um foguete, o efeito é interessante, embora simples.
Quando o tiro sai da tela sem atingir um avião, este é considerado um erro e o jogador é penalizado.

As bases são também desenhadas como objetos do jogo. Nesta versão eu mudei o desenho.

class Base(ObjetoDoJogo):
    def __init__(self, jogo, posicao):
        super(Base, self).__init__(jogo, cor=3)
        self.y = 175
        self.x = (posicao + 1) * 70

    def desenha(self):
        super().desenha()
        set_color(self.cor)
        pyglet.graphics.draw(5, pyglet.gl.GL_LINE_STRIP,
                             ('v2f', (self.x - 5, self.y + 5,
                                      self.x - 3, self.y + 2,
                                      self.x, self.y,
                                      self.x + 3, self.y + 2,
                                      self.x + 5, self.y + 5,
                                      )))

A classe do jogo em si foi simplificada:

class Game:
    def __init__(self, parent):
        self.cor = CORES
        self.window = parent
        self.tiros_disparados = 0  # tiros já disparados
        self.estado = "jogando"
        self.pressionado = False
        self.label = None
        self.objetos = [Aviao(self)]
        self.objetos.extend([Base(self, base) for base in range(0, 3)])
        self.pontos = 0

        self.placar = pyglet.text.Label("Pontos: 0", font_name="Times New Roman", font_size=16,
                                   x=80, y=-185 * 4, anchor_x='center', anchor_y='center')

Agora temos um contador de pontos e um placar. Um objeto da classe Avião e as três bases são adicionadas ao jogo inicial.

def desativa(self, objeto):
        self.objetos.remove(objeto)
        if objeto.colisao == AVIAO:
            if objeto.escapou:
                self.pontos -= 3
            else:
                self.pontos += 5
            self.objetos.append(Aviao(self))
        elif objeto.colisao == TIRO:
            if objeto.errou:
                self.pontos -= 1

A desativação de um objeto é a retirada deste da lista de objetos do jogo.
Quando é um avião, verifica se o avião escapou da tela sem ter sido atingido.
Se escapou, o jogador perde 3 pontos. Se foi destruído, o jogador ganha 5 pontos.

A cada tiro disparado que sai da tela sem atingir um avião, o jogador perde 1 ponto.

Agora que temos o placar a mostrar, precisamos modificar o método mostra_mensagem.
Este deve exibir o placar sempre:

    def mostra_mensagem(self):
        glPushMatrix()
        glScalef(0.25, -0.25, 0.25)
        if self.label:
            self.label.draw()
        self.placar.text = "Pontos: {}".format(self.pontos)
        self.placar.draw()
        glPopMatrix()


O glPushMatrix e glPopMatrix foram utilizados para corrigir o desenho do texto.
Como estamos utilizando uma escala 4 com inversão das coordenadas do eixo Y, o glScalef
foi utilizado com 0.25 (que na multiplicação é o mesmo que 1/4 ou que dividir por 4) e
-0.25 no eixo Y, que divide por 4 e inverte o sinal.
O glPushMatrix é utilizado para salvar a matriz atual na pilha, desta forma o glScalef
será utilizado apenas até o glPopMatrix que restaura os parâmetros anteriores.

O método atualiza foi simplificado:

    def atualiza(self):
        self.jogue()
        if self.estado != "jogando":
            if self.pressionado:
                pyglet.app.event_loop.exit()
        self.mostra_mensagem()

O método jogue sempre é chamado. Desta forma podemos ter o desenho do jogo, mesmo quando exibimos uma mensagem de fim por exemplo. Nesta versão, não há mudança de estado, pois eu modifiquei o jogo para gerar outro avião sempre que um for destruído ou sumir, não tendo fim.

E o método jogue também simplificado:

    def jogue(self):
        for objeto in self.objetos:
            objeto.desenha()

        self.processa_tiros()
        if self.estado == "jogando":
            for objeto in self.objetos:
                objeto.atualiza()

            if self.pressionado:
                self.objetos.append(Tiro(self, self.tiros_disparados % 3))
                self.tiros_disparados += 1
                self.pressionado = False

Ele apenas desenha todos os objetos da lista e chama processa_tiros que testa colisões.
Se ainda estivermos jogando, chama a atualização de todos os objetos da lista e se algo foi pressionado, gera um tiro. Para termos vários tiros, a base que o dispara é calculada pelo módulo do número do tiro e o número de bases.

def processa_tiros(self):
        tiros = [objeto for objeto in self.objetos if objeto.colisao == TIRO]
        avioes = [objeto for objeto in self.objetos if objeto.colisao == AVIAO]

        for aviao in avioes:
            for tiro in tiros:
                aviao.colide(tiro)

Aqui uma simplificação, onde abuso um pouco do computador :-D
Utilizando duas "list comprehensions", eu filtro a lista de objetos, procurando aviões e tiros.
Desta forma, a verificação de colisão se torna mais simples, pois basta verificar se o avião colidiu com um dos tiros.

O jogo principal pouco mudou, salvo o método on_key_press:

def on_key_press(self, symbol, modifiers):        
        if symbol == key.S:
            print("Placar: {}".format(self.game.pontos))
            pyglet.app.event_loop.exit()
        elif symbol == key.R:
            self.game = Game(self)
        else:
            self.game.pressionado = True

No caso, o jogo termina quando pressionamos a tecla S e reinicia se pressionarmos a tecla R.

O jogo completo ficou assim:


Video:



Para o próximo post:

  1. Sons
  2. Imagens


A pyglet não é atualizada há bastante tempo. Eu devo procurar outra biblioteca gráfica para outros posts, mas vou terminar as melhorias deste jogo ainda com pyglet e abusando da OpenGL.


sábado, 21 de maio de 2016

Convertendo um jogo escrito em Basic para Python - Parte II

Neste segundo post, vamos melhorar nosso jogo.

Embora a nova versão rode em Python, ainda está muito anos 80.
A primeira coisa a corrigir é a animação. Como estamos usando um sistema de coordenadas de 280 por 192 pontos para simular as coordenadas do Apple, multiplicamos cada coordenada por 4 na hora de desenhar. Para deixar parecido com o Apple, eu reduzi o número de frames a 8 frames por segundo. Por isso a animação dá tantos saltos! Para fazer com que rode a 60 frames, temos que multiplicar as velocidades pela razão entre o número de frames antigos e o novo: 8/60. A nova versão define algumas constantes para isso:

FRAMES_ORIGINAL = 8
FRAMES_NOVO = 60
RATIO = FRAMES_ORIGINAL / FRAMES_NOVO


Olhando o código, fica fácil perceber que o tiro, o avião e as bases são objetos. Vamos extrair o código do jogo e dividir as operações em atualiza e desenha. No caso, atualiza calcula a nova posição do objeto e desenha realiza o desenho em si.

class Aviao:
    def __init__(self, jogo, cor=3):
        self.cor = cor
        self.velocidade_aviao = (random.randint(0, 6) + 4) * RATIO
        self.altura_do_aviao = 159 - (random.randint(0, 135) + 11)
        self.posicao_do_aviao = self.velocidade_aviao
        self.ativo = True
        self.jogo = jogo

    def desenha(self):
        y = self.altura_do_aviao
        x = self.posicao_do_aviao
        set_color(self.cor)
        pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                             ('v2f', (x, y,
                                      x, y - 8,
                                      x + 3, y - 2,
                                      x + 12, y - 2,
                                      x + 14, y,
                                      x, y)))

    def desativa(self):
        self.ativo = False
        self.jogo.desativa(self)

    def atualiza(self):
        self.posicao_do_aviao += self.velocidade_aviao
        if self.posicao_do_aviao > 265:
            self.desativa()



 O código completo pode ser visto aqui:
Tente entender como a forma de desenhar em etapas, separando o desenho da atualização, permite que exibamos a mensagem com a imagem do jogo no fundo, ao invés da tela preta..

Os próximos passos são:

  1. Limpar as classes
  2. Vários aviões
  3. Múltiplos tiros.
  4. Generalizar os objetos do jogo em uma super classe.
  5. Exibir um placar
  6. Atribuir teclas para atirar e jogar de novo ou sair.


Até o próximo!


sábado, 14 de maio de 2016

Convertendo um jogo escrito em Basic para Python - Parte I

A nostalgia dos computadores da década de 80 é algo que nunca parei de ter. Quando criança, tive a sorte de utilzar vários computadores de 8-bits, como ZX-81, ZX-Spectrum, Apple II e MSX, ou melhor, seus clones nacionais (TK-85, TK-90X, TK2000), uma vez que o Brasil vivia a época da Reserva do Mercado de Informática.
Numa época que não havia Internet, nós passávamos o tempo a digitar programas. Uma série de livros sobre programação de jogos foi editada pela editora Lutécia no Brasil, mas os originais americanos foram liberados pela Usborne. Neste artigo, eu vou traduzir o jogo principal do Computer Battlegames, chamado de Missile, de Apple II Basic para Python com Pyglet. A listagem original está na página 34 do livro em inglês (ver pdf acima).

10 HOME
20 HGR
30 HCOLOR=3
40 DIM Y(3),F(3)
50 N=1 : MS=5
60 PS=INT(RND(1)*6+4)
70 P=INT(RND(1)*135+11)
80 GOSUB 400
90 FOR I=PS TO 265 STEP PS
100 X=I-PS : Y=159-P : C=0 : GOSUB 300
110 X=I : C=3: GOSUB 300
120 F$="" : IF PEEK(-16384)>127 THEN GET F$
130 IF F$="" OR N>3 THEN 160
140 F(N)=1
150 N=N+1
160 FOR J=1 TO 3
170 C=0 : GOSUB 350
180 IF F(J)=0 OR Y(J)>145 THEN 230
190 Y(J)=Y(J)+MS
200 C=3 : GOSUB 350
210 X=J*70-I : Y=P-Y(J)
220 IF X>-1 AND X<15 AND Y>-9 AND Y<5 THEN 270
230 NEXT
240 NEXT
250 VTAB 22 : PRINT "MISSED"
260 END
270 VTAB 22 : PRINT "HIT!!!"
280 END
300 HCOLOR=C
310 HPLOT X,Y TO X,Y-8
320 HPLOT TO X+3,Y-2 : HPLOT TO X+12, Y-2
330 HPLOT TO X+14,Y : HPLOT TO X,Y
340 RETURN
350 HCOLOR=C
360 HPLOT 70*J,158-Y(J) TO 70*J,154-Y(J)
340 RETURN
350 HCOLOR=C
360 HPLOT 70*J, 158-Y(J) TO 70*J,154-Y(J)
370 RETURN
400 FOR J=1 TO 3
410 HPLOT 70*J-5,159 TO 70*J+5,159
420 NEXT
430 RETURN
Vejamos o jogo rodando em um emulador:

Versão comentada:
# Limpa a tela
10 HOME
# Entra no modo de alta resolução 280x192
20 HGR
# Seleciona a cor 3 (purpura/magenta)
30 HCOLOR=3
# Cria dois vetores com 3 elementos cada
40 DIM Y(3),F(3)
# Inicializa N igual a 1 e MS igual a 5
50 N=1 : MS=5
# Gera um número aleatório entre 0 e 6 + 4
60 PS=INT(RND(1)*6+4)
# Gera um número aleatório entre 0 e 135 + 11
70 P=INT(RND(1)*135+11)
# Desvia para subrotina
80 GOSUB 400
# Loop: repete I de PS até 265, incrementando de PS
90 FOR I=PS TO 265 STEP PS
# Calcula os valores de X, Y e C. Desvia para subrotina em 300
100 X=I-PS : Y=159-P : C=0 : GOSUB 300
# Define X e C. Desvia para subrotina 300
110 X=I : C=3: GOSUB 300
# Verifica se uma tecla foi pressionada. Se foi, guarda em F
120 F$="" : IF PEEK(-16384)>127 THEN GET F$
# Se algo foi pressionado ou se N>3 desvia para 160
130 IF F$="" OR N>3 THEN 160
# F[N] = 1
140 F(N)=1
# N+=1
150 N=N+1
# Repete J de 1 até 3
160 FOR J=1 TO 3
# Zera C e desvia para subrotina da linha 350
170 C=0 : GOSUB 350
# Se F[J]==0 ou Y[J]>145 desvia para 230
180 IF F(J)=0 OR Y(J)>145 THEN 230
# Y[J]+=MS
190 Y(J)=Y(J)+MS
# C=3. Desvia para subrotina da linha 350
200 C=3 : GOSUB 350
# Calcula X e Y
210 X=J*70-I : Y=P-Y(J)
# Se X>-1 e X<15 e Y>-9 e Y<5 desvia para 270
220 IF X>-1 AND X<15 AND Y>-9 AND Y<5 THEN 270
# Fim do loop, volta para o for da linha 160
230 NEXT
# Fim do loop, volta para for da linha 90
240 NEXT
# Posiciona o cursor na linha 22 e imprime MISSED
250 VTAB 22 : PRINT "MISSED"
# Termina o programa
260 END
# Posiciona o cursor na linha 22 e imprime HIT!!!
270 VTAB 22 : PRINT "HIT!!!"
# Termina o programa
280 END
# Troca a cor de desenho para C
300 HCOLOR=C
# Traça uma linha de X,Y até X,Y-8
310 HPLOT X,Y TO X,Y-8
# Continua a linha
320 HPLOT TO X+3,Y-2 : HPLOT TO X+12, Y-2
330 HPLOT TO X+14,Y : HPLOT TO X,Y
# Retorno da subrotina
340 RETURN
# Troca a cor de desenho para C
350 HCOLOR=C
# Desenha linha
360 HPLOT 70*J,158-Y(J) TO 70*J,154-Y(J)
# Volta da subrotina
340 RETURN
# Troca a cor para C
350 HCOLOR=C
# Desenha linha
360 HPLOT 70*J, 158-Y(J) TO 70*J,154-Y(J)
# Volta da subrotina
370 RETURN
# Repete J de 1 à 3
400 FOR J=1 TO 3
# Desenha linha
410 HPLOT 70*J-5,159 TO 70*J+5,159
# Fim do loop, volta para linha 400
420 NEXT
# Retorna da subrotina
430 RETURN
Bom, como precisaremos desenhar, vamos instalar a Pyglet:
pip3 install pyglet

A primeira coisa a fazer é criar a janela da Pyglet e separar a janela do jogo em si. No caso, a janela é responsável por receber os eventos do teclado. A configuração do OpenGL também precisam ser feitas aqui. Como a alta resolução do Apple II é muito pequena 280x192 pontos, eu estou usando uma escala 4. Desta forma, as coordenadas permanecerão as mesmas, mas os gráficos serão 4 vezes maiores. Se ficar muito grande em seu monitor, você pode ajustar a escala.
Eu tentei manter os comentários originais para ficar mais fácil de relacionar o código novo com o antigo.
Outra mudança são as coordenadas Y. No Apple II, Y = 0 é a primeira linha e em OpenGL é a última.
Vejamos como ficou o código da janela:
class Missile(pyglet.window.Window):
    # 20 HGR
    def __init__(self):
        self.scale = 4    # Escala os gráficos 4x
        self.frames = 8   # Frames por segundo.
                          # Define quantas vezes vamos atualizar a tela por segundo
        super(Missile, self).__init__(280 * self.scale,
                                      192 * self.scale,
                                      caption="Missiles")
        self.set_mouse_visible(False)
        self.game = Game(self)
        self.schedule = pyglet.clock.schedule_interval(
            func=self.update, interval=1.0 / self.frames)

    def update(self, interval):
        pass

    def on_draw(self):
        window.clear()
        self.game.atualiza()

    def on_resize(self, width, height):
        # Inicializa a view
        glViewport(0, 0, width, height)
        glMatrixMode(gl.GL_PROJECTION)
        glLoadIdentity()
        # Inverte as coordenadas do eixo Y
        glOrtho(0, width, height, 0, -1, 1)
        # Aplica a escala
        glScalef(self.scale, self.scale, 1.0)
        glMatrixMode(gl.GL_MODELVIEW)

    def on_key_press(self, symbol, modifiers):
        # Verifica se uma tecla foi pressionada. Se foi, guarda em F
        # 120 F$="" : IF PEEK(-16384)>127 THEN GET F$
        self.game.pressionado = True

    def crialabel(self, mensagem, x=None, y=None,
                  fonte='Times New Roman', tamanho=36):
        """Prepara uma mensagem de texto para ser exibida"""
        x = x or self.width // 2
        y = y or self.height // 2
        return pyglet.text.Label(
            mensagem, font_name=fonte, font_size=tamanho,
            x=x, y=y, anchor_x='center', anchor_y='center')

Além disso, as cores do Apple II precisam ser definidas:
def rgb_to_f(r, g, b):
    return(r / 255.0, g / 255.0, b / 255.0)

# Cores do modo de alta resolução HGR
# Fonte: https://github.com/AppleWin/AppleWin/issues/254
CORES = [(0.0, 0.0, 0.0),  # Preto 0
         rgb_to_f(20, 245, 60),  # Verde 1
         rgb_to_f(255, 68, 253),  # Magenta 2
         rgb_to_f(255, 255, 255),  # Branco 3
         rgb_to_f(255, 106, 60),  # Laranja 5
         rgb_to_f(20, 207, 253),  # Azul médio 6
         rgb_to_f(255, 255, 255),  # Branco 7
         ]

e finalmente a classe Game com o jogo em si. Observar que os índices em Python começam em 0 e em Basic começam com 1. Primeiro passo da conversão:
class Game():
    def __init__(self, parent):
        self.cor = CORES
        self.window = parent
        # Cria dois vetores com 3 elementos cada
        # 40 DIM Y(3),F(3)
        self.Y = [0] * 3
        self.F = [0] * 3
        # Inicializa N igual a 1 e MS igual a 5
        # 50 N=1 : MS=5
        self.N = 0
        self.MS = 5
        # Gera um número aleatório entre 0 e 6 + 4
        # 60 PS=INT(RND(1)*6+4)
        self.PS = random.randint(0, 6) + 4
        # Gera um número aleatório entre 0 e 135 + 11
        # 70 P=INT(RND(1)*135+11)
        self.P = random.randint(0, 135) + 11
        self.estado = "jogando"
        self.I = self.PS
        self.pressionado = False
        self.label = None

    def atualiza(self):
        if self.estado == "jogando":
            self.jogue()
        else:
            if self.label:
                glMatrixMode(gl.GL_PROJECTION)
                glLoadIdentity()
                glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                glMatrixMode(gl.GL_MODELVIEW)
                self.label.draw()
            if self.pressionado:
                pyglet.app.event_loop.exit()

    def jogue(self):
        # Desvia para subrotina
        # 80 GOSUB 400
        self.sub_400()
        # Loop: repete I de PS até 265, incrementando de PS
        # 90 FOR I=PS TO 265 STEP PS
        if self.I > 265:
            self.sub_250()
            return
        # Calcula os valores de X, Y e C. Desvia para subrotina em 300
        # 100 X=I-PS : Y=159-P : C=0 : GOSUB 300
        self.X = self.I - self.PS
        self.y = 159 - self.P
        self.C = 0
        self.sub_300()
        # Define X e C. Desvia para subrotina 300
        # 110 X=I : C=3: GOSUB 300
        self.X = self.I
        self.C = 3
        self.sub_300()
        # Se algo foi pressionado ou se N>3 desvia para 160
        # 130 IF F$="" OR N>3 THEN 160
        if self.pressionado and self.N < 3:
            # F[N] = 1
            # 140 F(N)=1
            self.F[self.N] = 1
            # N+=1
            # 150 N=N+1
            self.N += 1
        self.pressionado = False
        # Repete J de 1 até 3
        # 160 FOR J=1 TO 3
        for self.J in range(0, 3):
            # Zera C e desvia para subrotina da linha 350
            # 170 C=0 : GOSUB 350
            self.C = 0
            self.sub_350()
            # Se F[J]==0 ou Y[J]>145 desvia para 230
            # 180 IF F(J)=0 OR Y(J)>145 THEN 230
            if self.F[self.J] == 0 or self.Y[self.J] > 145:
                continue
            # Y[J]+=MS
            # 190 Y(J)=Y(J)+MS
            self.Y[self.J] += self.MS
            # C=3. Desvia para subrotina da linha 350
            # 200 C=3 : GOSUB 350
            self.C = 3
            self.sub_350()
            # Calcula X e Y
            # 210 X=J*70-I : Y=P-Y(J)
            self.X = (self.J + 1) * 70 - self.I
            self.y = self.P - self.Y[self.J]
            # Se X>-1 e X<15 e Y>-9 e Y<5 desvia para 270
            # 220 IF X>-1 AND X<15 AND Y>-9 AND Y<5 THEN 270
            if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                self.sub_270()           
            # Fim do loop, volta para o for da linha 160
            # 230 NEXT
            # Fim do loop, volta para for da linha 90
            # 240 NEXT
        # 90 FOR I=PS TO 265 STEP PS
        self.I += self.PS

    def sub_250(self):
        # Posiciona o cursor na linha 22 e imprime MISSED
        # 250 VTAB 22 : PRINT "MISSED"
        print("MISSED")
        self.imprima("MISSED")
        # Termina o programa
        # 260 END
        self.muda_estado("fimdejogo")

    def sub_270(self):
        # Posiciona o cursor na linha 22 e imprime HIT!!!
        # 270 VTAB 22 : PRINT "HIT!!!"
        print("HIT")
        self.imprima("HIT!!!")
        # Termina o programa
        self.muda_estado("fimdejogo")

    def sub_300(self):
        # Troca a cor de desenho para C
        # 300 HCOLOR=C
        self.set_color(self.C)
        # Traça uma linha de X,Y até X,Y-8
        # 310 HPLOT X,Y TO X,Y-8
        # Continua a linha
        # 320 HPLOT TO X+3,Y-2 : HPLOT TO X+12, Y-2
        # 330 HPLOT TO X+14,Y : HPLOT TO X,Y
        pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                             ('v2i', (self.X, self.y,
                                      self.X, self.y - 8,
                                      self.X + 3, self.y - 2,
                                      self.X + 12, self.y - 2,
                                      self.X + 14, self.y,
                                      self.X, self.y)))

        # Retorno da subrotina
        # 340 RETURN

    def sub_350(self):
        # Troca a cor para C
        # 350 HCOLOR=C
        self.set_color(self.C)
        # Desenha linha
        # 360 HPLOT 70*J, 158-Y(J) TO 70*J,154-Y(J)
        J = self.J + 1
        pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                             ('v2i', (70 * J, 158 - self.Y[self.J],
                                      70 * J, 154 - self.Y[self.J])))
        # Volta da subrotina
        # 370 RETURN

    def sub_400(self):
        self.set_color(3)
        # 400 FOR J=1 TO 3
        for J in range(1, 4):
            # Desenha linha
            # 410 HPLOT 70*J-5,159 TO 70*J+5,159
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J - 5, 159,
                                          70 * J + 5, 159)))
            # Fim do loop, volta para linha 400
            # 420 NEXT
        # Retorna da subrotina
        # 430 RETURN

    def set_color(self, color):
        glColor3f(*self.cor[color])

    def muda_estado(self, estado):
        print("Mudança de Estado: {} --> {}".format(self.estado, estado))
        self.estado = estado

    def imprima(self, mensagem):
        self.label = self.window.crialabel(mensagem)

Como em OpenGL limpamos a tela a cada frame, a rotina que apaga os objetos pode ser apagada. No caso, a chamada a sub_300() com C=0.
O nome dos métodos ainda não foram mudados, vamos renomear:
sub_250 para pedeu
sub_270 para acertou
sub_300 para desenha_aviao
sub_350 para desenha_tiro
sub_400 para desenha_bases
Apagando os comentários com o código em Basic, temos:
import pyglet
from pyglet.gl import *
import random


def rgb_to_f(r, g, b):
    return(r / 255.0, g / 255.0, b / 255.0)

# Cores do modo de alta resolução HGR
# Fonte: https://github.com/AppleWin/AppleWin/issues/254
CORES = [(0.0, 0.0, 0.0),  # Preto 0
         rgb_to_f(20, 245, 60),  # Verde 1
         rgb_to_f(255, 68, 253),  # Magenta 2
         rgb_to_f(255, 255, 255),  # Branco 3
         rgb_to_f(255, 106, 60),  # Laranja 5
         rgb_to_f(20, 207, 253),  # Azul médio 6
         rgb_to_f(255, 255, 255),  # Branco 7
         ]


class Game():
    def __init__(self, parent):
        self.cor = CORES
        self.window = parent
        # Cria dois vetores com 3 elementos cada
        self.Y = [0] * 3
        self.F = [0] * 3
        # Inicializa N igual a 1 e MS igual a 5
        self.N = 0
        self.MS = 5
        # Gera um número aleatório entre 0 e 6 + 4
        self.PS = random.randint(0, 6) + 4
        # Gera um número aleatório entre 0 e 135 + 11
        self.P = random.randint(0, 135) + 11
        self.estado = "jogando"
        self.I = self.PS
        self.pressionado = False
        self.label = None

    def atualiza(self):
        if self.estado == "jogando":
            self.jogue()
        else:
            if self.label:
                glMatrixMode(gl.GL_PROJECTION)
                glLoadIdentity()
                glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                glMatrixMode(gl.GL_MODELVIEW)
                self.label.draw()
            if self.pressionado:
                pyglet.app.event_loop.exit()

    def jogue(self):
        self.desenha_bases()
        if self.I > 265:
            self.perdeu()
            return
        self.X = self.I - self.PS
        self.y = 159 - self.P
        self.C = 0
        self.desenha_aviao()
        self.X = self.I
        self.C = 3
        self.desenha_aviao()
        if self.pressionado and self.N < 3:
            self.F[self.N] = 1
            self.N += 1
        self.pressionado = False
        for self.J in range(0, 3):
            if self.F[self.J] == 0 or self.Y[self.J] > 145:
                continue
            self.Y[self.J] += self.MS
            self.C = 3
            self.desenha_tiro()
            self.X = (self.J + 1) * 70 - self.I
            self.y = self.P - self.Y[self.J]
            if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                self.acertou()
        self.I += self.PS

    def perdeu(self):
        self.imprima("MISSED")
        self.muda_estado("fimdejogo")

    def acertou(self):
        self.imprima("HIT!!!")
        self.muda_estado("fimdejogo")

    def desenha_aviao(self):
        self.set_color(self.C)
        pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                             ('v2i', (self.X, self.y,
                                      self.X, self.y - 8,
                                      self.X + 3, self.y - 2,
                                      self.X + 12, self.y - 2,
                                      self.X + 14, self.y,
                                      self.X, self.y)))

    def desenha_tiro(self):
        self.set_color(self.C)
        J = self.J + 1
        pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                             ('v2i', (70 * J, 158 - self.Y[self.J],
                                      70 * J, 154 - self.Y[self.J])))

    def desenha_bases(self):
        self.set_color(3)
        for J in range(1, 4):
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J - 5, 159,
                                          70 * J + 5, 159)))

    def set_color(self, color):
        glColor3f(*self.cor[color])

    def muda_estado(self, estado):
        print("Mudança de Estado: {} --> {}".format(self.estado, estado))
        self.estado = estado

    def imprima(self, mensagem):
        self.label = self.window.crialabel(mensagem)


class Missile(pyglet.window.Window):
    def __init__(self):
        self.scale = 4    # Escala os gráficos 4x
        self.frames = 8   # Frames por segundo.
                          # Define quantas vezes vamos atualizar a tela por segundo
        super(Missile, self).__init__(280 * self.scale,
                                      192 * self.scale,
                                      caption="Missiles")
        self.set_mouse_visible(False)
        self.game = Game(self)
        self.schedule = pyglet.clock.schedule_interval(
            func=self.update, interval=1.0 / self.frames)

    def update(self, interval):
        pass

    def on_draw(self):
        window.clear()
        self.game.atualiza()

    def on_resize(self, width, height):
        # Inicializa a view
        glViewport(0, 0, width, height)
        glMatrixMode(gl.GL_PROJECTION)
        glLoadIdentity()
        # Inverte as coordenadas do eixo Y
        glOrtho(0, width, height, 0, -1, 1)
        # Aplica a escala
        glScalef(self.scale, self.scale, 1.0)
        glMatrixMode(gl.GL_MODELVIEW)

    def on_key_press(self, symbol, modifiers):
        self.game.pressionado = True

    def crialabel(self, mensagem, x=None, y=None,
                  fonte='Times New Roman', tamanho=36):
        """Prepara uma mensagem de texto para ser exibida"""
        x = x or self.width // 2
        y = y or self.height // 2
        return pyglet.text.Label(
            mensagem, font_name=fonte, font_size=tamanho,
            x=x, y=y, anchor_x='center', anchor_y='center')


window = Missile()
pyglet.app.run()

Bem melhor, mas ainda guarda várias características do programa em Basic. O nome das variáveis é uma catástrofe. Renomeando as variáveis e melhorando os comentários, o programa completo fica assim:
import pyglet
from pyglet.gl import *
import random


def rgb_to_f(r, g, b):
    return(r / 255.0, g / 255.0, b / 255.0)

# Cores do modo de alta resolução HGR
# Fonte: https://github.com/AppleWin/AppleWin/issues/254
CORES = [(0.0, 0.0, 0.0),  # Preto 0
         rgb_to_f(20, 245, 60),  # Verde 1
         rgb_to_f(255, 68, 253),  # Magenta 2
         rgb_to_f(255, 255, 255),  # Branco 3
         rgb_to_f(255, 106, 60),  # Laranja 5
         rgb_to_f(20, 207, 253),  # Azul médio 6
         rgb_to_f(255, 255, 255),  # Branco 7
         ]


class Game():
    def __init__(self, parent):
        self.cor = CORES
        self.window = parent
        # Cria dois vetores com 3 elementos cada
        self.Y = [0] * 3 # Altura do tiro
        self.F = [0] * 3 # Estado do tiro
        self.tiros_disparados = 0  # tiros já disparados
        self.velocidade_do_tiro = 5
        # Gera um número aleatório entre 0 e 6 + 4
        self.velocidade_aviao = random.randint(0, 6) + 4
        # Gera um número aleatório entre 0 e 135 + 11
        self.altura_do_aviao = random.randint(0, 135) + 11
        self.estado = "jogando"
        self.posicao_do_aviao = self.velocidade_aviao
        self.pressionado = False
        self.label = None

    def atualiza(self):
        if self.estado == "jogando":
            self.jogue()
        else:
            if self.label:
                glMatrixMode(gl.GL_PROJECTION)
                glLoadIdentity()
                glOrtho(0, self.window.width, 0, self.window.height, -1, 1)
                glMatrixMode(gl.GL_MODELVIEW)
                self.label.draw()
            if self.pressionado:
                pyglet.app.event_loop.exit()

    def jogue(self):
        self.desenha_bases()
        if self.posicao_do_aviao > 265:
            self.perdeu()
            return
        self.y = 159 - self.altura_do_aviao
        self.X = self.posicao_do_aviao
        self.C = 3
        self.desenha_aviao()
        if self.pressionado and self.tiros_disparados < 3:
            self.F[self.tiros_disparados] = 1
            self.tiros_disparados += 1
        self.pressionado = False
        self.processa_tiros()
        self.posicao_do_aviao += self.velocidade_aviao

    def processa_tiros(self):
        for self.J in range(0, 3):
            if self.F[self.J] == 0 or self.Y[self.J] > 145:
                continue
            self.Y[self.J] += self.velocidade_do_tiro
            self.C = 3
            self.desenha_tiro()
            self.X = (self.J + 1) * 70 - self.posicao_do_aviao
            self.y = self.altura_do_aviao - self.Y[self.J]
            if self.X > -1 and self.X < 15 and self.y > -9 and self.y < 5:
                self.acertou()

    def perdeu(self):
        self.imprima("MISSED")
        self.muda_estado("fimdejogo")

    def acertou(self):
        self.imprima("HIT!!!")
        self.muda_estado("fimdejogo")

    def desenha_aviao(self):
        self.set_color(self.C)
        pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP,
                             ('v2i', (self.X, self.y,
                                      self.X, self.y - 8,
                                      self.X + 3, self.y - 2,
                                      self.X + 12, self.y - 2,
                                      self.X + 14, self.y,
                                      self.X, self.y)))

    def desenha_tiro(self):
        self.set_color(self.C)
        J = self.J + 1
        pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                             ('v2i', (70 * J, 158 - self.Y[self.J],
                                      70 * J, 154 - self.Y[self.J])))

    def desenha_bases(self):
        self.set_color(3)
        for J in range(1, 4):
            pyglet.graphics.draw(2, pyglet.gl.GL_LINES,
                                 ('v2i', (70 * J - 5, 159,
                                          70 * J + 5, 159)))

    def set_color(self, color):
        glColor3f(*self.cor[color])

    def muda_estado(self, estado):
        print("Mudança de Estado: {} --> {}".format(self.estado, estado))
        self.estado = estado

    def imprima(self, mensagem):
        self.label = self.window.crialabel(mensagem)


class Missile(pyglet.window.Window):
    def __init__(self):
        self.scale = 4    # Escala os gráficos 4x
        self.frames = 8   # Frames por segundo.
                          # Define quantas vezes vamos atualizar a tela por segundo
        super(Missile, self).__init__(280 * self.scale,
                                      192 * self.scale,
                                      caption="Missiles")
        self.set_mouse_visible(False)
        self.game = Game(self)
        self.schedule = pyglet.clock.schedule_interval(
            func=self.update, interval=1.0 / self.frames)

    def update(self, interval):
        pass

    def on_draw(self):
        window.clear()
        self.game.atualiza()

    def on_resize(self, width, height):
        # Inicializa a view
        glViewport(0, 0, width, height)
        glMatrixMode(gl.GL_PROJECTION)
        glLoadIdentity()
        # Inverte as coordenadas do eixo Y
        glOrtho(0, width, height, 0, -1, 1)
        # Aplica a escala
        glScalef(self.scale, self.scale, 1.0)
        glMatrixMode(gl.GL_MODELVIEW)

    def on_key_press(self, symbol, modifiers):
        self.game.pressionado = True

    def crialabel(self, mensagem, x=None, y=None,
                  fonte='Times New Roman', tamanho=36):
        """Prepara uma mensagem de texto para ser exibida"""
        x = x or self.width // 2
        y = y or self.height // 2
        return pyglet.text.Label(
            mensagem, font_name=fonte, font_size=tamanho,
            x=x, y=y, anchor_x='center', anchor_y='center')


window = Missile()
pyglet.app.run()

Em um próximo artigo, vou continuar a refatorar o código. Tiro e Avião são claramente classes. Animação deixa a desejar. Mas é o divertido da programação, você sempre pode melhorar.

quarta-feira, 30 de setembro de 2015

Ajude a manter a Fundação Nokia aberta

A Fundação Nokia é uma instituição de ensino médio, localizada em Manaus, Amazonas. Atuando há 30 anos na região, a instituição teve vários nomes como CEPI, FEPEMM, FMM e finalmente Fundação Nokia a partir de 2001. Com a venda da Nokia, a Fundação Nokia passou a ser mantida pela Microsoft.

Este ano, pela primeira vez em 30 anos, o processo seletivo da instituição foi suspenso.
Se esta situação continuar, a Fundação Nokia vai desaparecer em 3 anos.

Como ex-alunos desta instituição, pedimos que você entre em contato com nossos representantes, enviando um e-mail explicando suas razões para ajudar a manter a Fundação aberta.

Governo do Amazonas
http://www.amazonas.am.gov.br/fale-conosco/

Vereadores de Manaus:
http://www.cmm.am.gov.br/vereador/

Deputados Estaduais do Amazonas:
http://www.ale.am.gov.br/deputados/

Deputados Federais da bancada do Amazonas:
http://www.camara.gov.br/internet/deputado/Dep_Lista_foto.asp?Legislatura=55&Partido=QQ&SX=QQ&Todos=None&UF=AM&condic=QQ&forma=lista&nome=&ordem=nome&origem=None

Senadores do Amazonas:
http://www25.senado.leg.br/web/senadores/por-uf/-/uf/AM

Vamos tentar também via Twitter, eu já enviei alguns pelo @lskbr (retuites ajudam!), mas você também pode enviar diretamente ao CEO da Microsoft:

Satya Nadella - CEO Microsoft
@satyanadella

Para quem não conhece a Fundação, esta página tem várias novidades: http://www.fundacaonokia.org/noticias

Não podemos ficar calados e esperar que esta escola feche. Ajude a manter a Fundação Nokia aberta, solicitando a Microsoft mais tempo para encontrar uma outra instituição mantenedora e exigindo a presença de nossos políticos.

sábado, 30 de agosto de 2014

Servidor de Chat com websockets e asyncio

Já é hora de escrever sobre um projeto mais completo. Aproveito para mostrar o módulo websockets para Python 3.4 que funciona muito bem com asyncio. Para não ter problemas de interface, eu resolvi escrever o cliente do chat em JavaScript. O cliente de exemplo foi baixado daqui. Como quase sempre, os exemplos são muito simples e nos deixam com água na boca sobre o que poderíamos realmente fazer. Quem já tentou escrever um chat em JavaScript sabe que WebSockets são uma mão na roda.

Este artigo faz parte de uma série que escrevo sobre o asyncio do Python 3.4. Você pode ler os outros artigos clicando aqui: Python e asyncio, Asyncio e corotinas e o Lendo o teclado.

A ideia de usarmos WebSockets visa demonstrar a facilidade do módulo websockets que deve ser instalado no Python 3.4:

pip install websockets

Uma vez instalado o módulo, podemos criar um servidor, como o mostrado na documentação do módulo:

import asyncio
import websockets

@asyncio.coroutine
def hello(websocket, path):
    name = yield from websocket.recv()
    print("< {}".format(name))
    greeting = "Hello {}!".format(name)
    yield from websocket.send(greeting)
    print("> {}".format(greeting))

start_server = websockets.serve(hello, 'localhost', 8765)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Execute o servidor com:
py -3 server.py

Para testar este programa, precisaremos de uma página com nosso cliente de chat. Eu preparei uma página com todo o código html e javascript necessário. Baixe a página cliente.html aqui. Salve o arquivo e abra-o com seu browser preferido: Chrome, Firefox ou o IE (>10).

Como o servidor é muito simples, tudo que podemos fazer é enviar uma mensagem e recebê-la de volta, já que é um servidor do tipo Echo (eco). Veja também que o servidor de WebSockets é inicializado como nossos outros servidores com o módulo asyncio, mas que este não obedece uma interface com métodos definidos para conexão, recebimento de dados etc. O objeto retornado pelo módulo websockets é um objeto com o cliente já conectado. Vejamos primeiramente como criar o servidor:

start_server = websockets.serve(hello, 'localhost', 8765)

A linha acima cria nosso servidor, chamando a função hello sempre que um novo cliente for executado. O nome localhost se refere a nosso computador e 8765 é a porta que utilizaremos para receber as conexões. Vejamos a função hello. Quando uma nova conexão for recebida, a função hello será chamado com dois parâmetros: o primeiro é o cliente já conectado e pronto para ser utilizado; e o segundo é o path ou o caminho usado (veremos isso depois em outro artigo). Na realidade, a corotina hello é responsável pelo tempo de vida e gestão da conexão do cliente. Quando a corotina hello termina, o cliente é desconectado. Veja também que usamos o yield from para enviar e receber dados. O uso do yield from permite que escrevamos nosso código como se sua execução fosse sequencial, como já discutimos nos outros artigos.

Um detalhe importante a notar é que a interface do módulo websockets já entrega os dados no formato de mensagem (como definido pelo protocolo). Diferentemente de um socket TCP/IP comum que trabalha com streams, entregando bytes. Quando o método recv retorna, uma mensagem inteira foi recebida, pouca importa quantos read foram feitos para completar esta tarefa. Esta característica vai facilitar muito a implementação do servidor de chat, uma vez que não precisaremos inventar um delimitador de mensagem, nem separar as mensagens manualmente em nosso código.

Em relação a nosso servidor de chat, o servidor de exemplo é bem limitado. A maior limitação é não permitir a comunicação entre clientes. A ideia do servidor de chat é enviar mensagens a todos os clientes conectados. Desta forma, o servidor deve ser informado sobre e registrar todas as conexões e desconexões do sistema. Vamos manter a lista dos clientes conectados com uma lista. Nossos clientes serão controlados por uma classe Cliente que veremos mais tarde. Observe a implementação parcial da classe Servidor:

class Servidor:
    def __init__(self):
        self.conectados = []
    
    @property
    def nconectados(self):
        return len(self.conectados)
    
    @asyncio.coroutine
    def conecta(self, websocket, path):
        cliente = Cliente(self, websocket, path)
        if cliente not in self.conectados:
            self.conectados.append(cliente)
            print("Novo cliente conectado. Total: {0}".format(self.nconectados))
        yield from cliente.gerencia()

    def desconecta(self, cliente):
        if cliente in self.conectados:
            self.conectados.remove(cliente)
        print("Cliente {1} desconectado. Total: {0}".format(self.nconectados, cliente.nome))

Veja que apenas conecta é uma corotina. Na realidade, todo o trabalho é feito na classe Cliente, que disponibiliza o método gerencia como uma corotina. O importante agora é entender a manutenção da lista de conexões ativas.

Vejamos a classe Cliente (parcial):

class Cliente:    
    def __init__(self, servidor, websocket, path):
        self.cliente = websocket
        self.servidor = servidor
        self.nome = None        
    
    @property
    def conectado(self):
        return self.cliente.open

    @asyncio.coroutine
    def gerencia(self):
        try:
            yield from self.envia("Bem vindo ao servidor de chat escrito em Python 3.4 com asyncio e WebSockets. Identifique-se com /nome SeuNome")
            while True:
                mensagem = yield from self.recebe()
                if mensagem:
                    print("{0} < {1}".format(self.nome, mensagem))
                    yield from self.processa_comandos(mensagem)                                            
                else:
                    break
        except Exception:
            print("Erro")
            raise        
        finally:
            self.servidor.desconecta(self)

Como cada cliente tem seu próprio websocket e precisa se comunicar com o servidor, guardaremos estas referências como atributos. Já preparamos também a gestão de nomes, embora tenhamos inicializado o nome do Cliente com None. O método gerencia, que é uma corotina, envia uma mensagem de boas vindas ao cliente e como no exemplo anterior, utiliza yield from para realizar o envio no loop de eventos. Uma vez que a mensagem inicial é enviada, entramos em um loop infinito que espera uma mensagem do cliente. Quando a conexão é fechada ou acontece um erro, o valor de mensagem é igual a None, por isso, testamos o valor de mensagem para sair do loop infinito criado pelo while True. Da mesma forma que no primeiro servidor de exemplo, nosso cliente é desconectado quando a corotina gerencia termina. Aproveitamos o fim da corotina para informar ao servidor que este cliente está se desconectado.

Ao recebermos uma mensagem, iniciamos o processamento da mesma, utilizando o método corotina processa_comandos da classe Cliente:
    @asyncio.coroutine
    def processa_comandos(self, mensagem):        
        if mensagem.strip().startswith("/"):
            comandos=shlex.split(mensagem.strip()[1:])
            if len(comandos)==0:
                yield from self.envia("Comando inválido")
                return
            print(comandos)
            comando = comandos[0].lower()            
            if comando == "horas":
                yield from self.envia("Hora atual: " + time.strftime("%H:%M:%S"))
            elif comando == "data":
                yield from self.envia("Data atual: " + time.strftime("%d/%m/%y"))
            elif comando == "clientes":
                yield from self.envia("{0} clientes conectados".format(self.servidor.nconectados))
            elif comando == "nome":
                yield from self.altera_nome(comandos)
            elif comando == "apenas":
                yield from self.apenas_para(comandos)
            else:
                yield from self.envia("Comando desconhecido")
        else:
            if self.nome:
                yield from self.servidor.envia_a_todos(self, mensagem)
            else:
                yield from self.envia("Identifique-se para enviar mensagens. Use o comando /nome SeuNome")

Lembrando os bons velhos tempos do IRC, o método processa_comandos reconhece comandos iniciados pela barra /. Desta forma, caso um cliente envie para o servidor /horas, este retornará a hora atual do servidor. Implementamos também os comandos:


  • /data que envia a data atual;
  • /clientes que envia quantos clientes estão conectados ao servidor, 
  • /nome e /apenas que veremos mais adiante. 


Utilizamos o módulo shlex para simplificar o processamento dos comandos, uma vez que a função shlex.split permite processar uma linha de texto como uma linha de comandos do bash, reconhecendo valores entre aspas e retirando os espaços em branco entre os parâmetros. Caso o usuário envie uma mensagem que não se inicia por uma barra, esta mensagem será enviada a todos os outros usuários conectados.

Para melhorar nosso chat, utilizamos o comando /nome para configurar nosso nome. O servidor cuida para que apenas um usuário utilize cada nome, retornando uma mensagem de erro, caso o nome desejado já esteja em uso. Este comando é processado pelo método altera_nome da classe Cliente:

    @asyncio.coroutine
    def altera_nome(self, comandos):                
        if len(comandos)>1 and self.servidor.verifica_nome(comandos[1]):
            self.nome = comandos[1]
            yield from self.envia("Nome alterado com sucesso para {0}".format(self.nome))
        else:
            yield from self.envia("Nome em uso ou inválido. Escolha um outro.")

O método altera_nome simplesmente verifica se passamos um parâmetro depois do comando /nome, pois comandos é uma lista onde cada elemento é um parâmetro (mas o primeiro é o nome do comando em si). Usando o método verifica_nome do servidor, checamos se o nome é único e enviamos uma mensagem de confirmação ou de erro dependendo do resultado. O método verifica_nome da classe Servidor é apresentado abaixo:

    def verifica_nome(self, nome):
        for cliente in self.conectados:
            if cliente.nome and cliente.nome == nome:
                return False
        return True

A verificação percorre toda a lista com os clientes conectados e verifica se um nome igual já foi registrado. Caso não encontre o nome na lista dos clientes já conectados, retorna True, permitindo o registro do nome pelo cliente que o solicitou.

Um outro comando interessante é o  /apenas que permite enviarmos uma mensagem apenas para determinado cliente. Vejamos a implementação do método apenas_para no cliente:

    @asyncio.coroutine
    def apenas_para(self, comandos):
        if len(comandos)<3:
            yield from self.envia("Comando incorreto. /apenas Destinatário mensagem")
            return
        destinatario = comandos[1]
        mensagem = " ".join(comandos[2:])
        enviado = yield from self.servidor.envia_a_destinatario(self, mensagem, destinatario)
        if not enviado:
            yield from self.envia("Destinatário {0} não encontrado. Mensagem não enviada.".format(destinatario))

E do método que realiza o envio na classe Servidor:

    @asyncio.coroutine
    def envia_a_destinatario(self, origem, mensagem, destinatario):        
        for cliente in self.conectados:            
            if cliente.nome == destinatario and origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("PRIVADO de {0} >> {1}".format(origem.nome, mensagem))
                return True
        return False

Um outro método importante é o que envia uma mensagem a todos os clientes conectados:

    @asyncio.coroutine
    def envia_a_todos(self, origem, mensagem):
        print("Enviando a todos")
        for cliente in self.conectados:            
            if origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("{0} >> {1}".format(origem.nome, mensagem))

Veja que enviamos a mensagem a todos da lista, mas que tomamos o cuidado para não enviar a mensagem ao mesmo cliente que a enviou, pois esta seria impressa uma segunda vez e nosso cliente Javascript já fez este trabalho por nós. A listagem completa abaixo:

import asyncio
import websockets
import time
import shlex

class Servidor:
    def __init__(self):
        self.conectados = []
    
    @property
    def nconectados(self):
        return len(self.conectados)
    
    @asyncio.coroutine
    def conecta(self, websocket, path):
        cliente = Cliente(self, websocket, path)
        if cliente not in self.conectados:
            self.conectados.append(cliente)
            print("Novo cliente conectado. Total: {0}".format(self.nconectados))            
        yield from cliente.gerencia()

    def desconecta(self, cliente):
        if cliente in self.conectados:
            self.conectados.remove(cliente)
        print("Cliente {1} desconectado. Total: {0}".format(self.nconectados, cliente.nome))            

    @asyncio.coroutine
    def envia_a_todos(self, origem, mensagem):
        print("Enviando a todos")
        for cliente in self.conectados:            
            if origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("{0} >> {1}".format(origem.nome, mensagem))

    @asyncio.coroutine
    def envia_a_destinatario(self, origem, mensagem, destinatario):        
        for cliente in self.conectados:            
            if cliente.nome == destinatario and origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("PRIVADO de {0} >> {1}".format(origem.nome, mensagem))
                return True
        return False

    def verifica_nome(self, nome):
        for cliente in self.conectados:
            if cliente.nome and cliente.nome == nome:
                return False
        return True


class Cliente:    
    def __init__(self, servidor, websocket, path):
        self.cliente = websocket
        self.servidor = servidor
        self.nome = None        
    
    @property
    def conectado(self):
        return self.cliente.open

    @asyncio.coroutine
    def gerencia(self):
        try:
            yield from self.envia("Bem vindo ao servidor de chat escrito em Python 3.4 com asyncio e WebSockets. Identifique-se com /nome SeuNome")
            while True:
                mensagem = yield from self.recebe()
                if mensagem:
                    print("{0} < {1}".format(self.nome, mensagem))
                    yield from self.processa_comandos(mensagem)                                            
                else:
                    break
        except Exception:
            print("Erro")
            raise        
        finally:
            self.servidor.desconecta(self)

    @asyncio.coroutine
    def envia(self, mensagem):
        yield from self.cliente.send(mensagem)

    @asyncio.coroutine
    def recebe(self):
        mensagem = yield from self.cliente.recv()
        return mensagem

    @asyncio.coroutine
    def processa_comandos(self, mensagem):        
        if mensagem.strip().startswith("/"):
            comandos=shlex.split(mensagem.strip()[1:])
            if len(comandos)==0:
                yield from self.envia("Comando inválido")
                return
            print(comandos)
            comando = comandos[0].lower()            
            if comando == "horas":
                yield from self.envia("Hora atual: " + time.strftime("%H:%M:%S"))
            elif comando == "data":
                yield from self.envia("Data atual: " + time.strftime("%d/%m/%y"))
            elif comando == "clientes":
                yield from self.envia("{0} clientes conectados".format(self.servidor.nconectados))
            elif comando == "nome":
                yield from self.altera_nome(comandos)
            elif comando == "apenas":
                yield from self.apenas_para(comandos)
            else:
                yield from self.envia("Comando desconhecido")
        else:
            if self.nome:
                yield from self.servidor.envia_a_todos(self, mensagem)
            else:
                yield from self.envia("Identifique-se para enviar mensagens. Use o comando /nome SeuNome")

    @asyncio.coroutine
    def altera_nome(self, comandos):                
        if len(comandos)>1 and self.servidor.verifica_nome(comandos[1]):
            self.nome = comandos[1]
            yield from self.envia("Nome alterado com sucesso para {0}".format(self.nome))
        else:
            yield from self.envia("Nome em uso ou inválido. Escolha um outro.")

    @asyncio.coroutine
    def apenas_para(self, comandos):
        if len(comandos)<3:
            yield from self.envia("Comando incorreto. /apenas Destinatário mensagem")
            return
        destinatario = comandos[1]
        mensagem = " ".join(comandos[2:])
        enviado = yield from self.servidor.envia_a_destinatario(self, mensagem, destinatario)
        if not enviado:
            yield from self.envia("Destinatário {0} não encontrado. Mensagem não enviada.".format(destinatario))



servidor=Servidor()
loop=asyncio.get_event_loop()

start_server = websockets.serve(servidor.conecta, 'localhost', 8765)

try:
    loop.run_until_complete(start_server)
    loop.run_forever()
finally:
    start_server.close()

Você pode baixar o arquivo completo clicando aqui.

Para testar, abra o arquivo cliente.html que você já deve ter salvo em seu computador. Se você já ativou o servidor com

py -3 servidor2.py

nosso cliente já deve estar conectado. Caso contrário, recarregue a página no navegador para forçar a reconexão. Quando conectado, o cliente exibe uma barra azul no topo da página. Para simular vários clientes, abra várias vezes o arquivo cliente.html. Vejamos uma sessão simples com 3 clientes:

No primeiro cliente:
/nome Cliente1
/horas

No segundo cliente:
/nome Cliente2
/data

No terceiro cliente:
/nome Cliente3
/apenas Cliente1 Olá 1
/apenas Cliente2 Olá 2
Olá todos!

Não esqueça de copiar linha por linha nas respectivas janelas. Digite ENTER para enviar a mensagem, uma de cada vez. Divirta-se!


sábado, 23 de agosto de 2014

Asyncio - Lendo o teclado

Continuando a série sobre o módulo asyncio do Python 3.4, vamos ver como criar um jogo simples, em modo texto. O objetivo do jogo é simplesmente mostrar um labirinto e deixar o jogador se mover utilizando o teclado numérico (4 - esquerda, 6 - direita, 8 - cima, 2 - baixo e S para sair). Para exercitar nossos músculos da época do DOS com programação assíncrona, vamos exibir o relógio na última linha. O resultado final deve se parecer com a imagem abaixo:


Você precisa instalar o colorconsole e o Python 3.4.1 para executar este programa.

from random import shuffle, randrange
from colorconsole import terminal
from concurrent.futures import ThreadPoolExecutor
import datetime
import asyncio

# Colorconsole: https://github.com/lskbr/colorconsole
# Make_make: http://rosettacode.org/wiki/Maze_generation#Python
def make_maze(w = 16, h = 8):
    vis = [[0] * w + [1] for _ in range(h)] + [[1] * (w + 1)]
    ver = [["|  "] * w + ['|'] for _ in range(h)] + [[]]
    hor = [["+--"] * w + ['+'] for _ in range(h + 1)]
 
    def walk(x, y):
        vis[y][x] = 1
 
        d = [(x - 1, y), (x, y + 1), (x + 1, y), (x, y - 1)]
        shuffle(d)
        for (xx, yy) in d:
            if vis[yy][xx]: continue
            if xx == x: hor[max(y, yy)][x] = "+  "
            if yy == y: ver[y][max(x, xx)] = "   "
            walk(xx, yy)
 
    walk(randrange(w), randrange(h))
    maze = []
    for (a, b) in zip(hor, ver):
        maze.append("".join(a))
        maze.append("".join(b))
    return maze
 
class Jogo:
    LARGURA = 24
    ALTURA = 11
    def __init__(self):        
        self.tela = terminal.get_terminal(conEmu=False)
        self.tela.enable_unbuffered_input_mode()
        self.labirinto_cores = (terminal.colors["RED"],terminal.colors["BLACK"])
        self.jogador_carac = (terminal.colors["WHITE"],terminal.colors["BLUE"],'*')
        self.labirinto = make_maze(Jogo.LARGURA,Jogo.ALTURA)
        self.loop = asyncio.get_event_loop()
        self.tpool = ThreadPoolExecutor(max_workers=2)
        while True:
            self.x = randrange(Jogo.LARGURA*3)
            self.y = randrange(Jogo.ALTURA*2)
            if self.pode_mover(self.x, self.y):
                break
        self.jogando = True

    def fim_do_jogo(self):
        self.jogando = False
        self.loop.stop()

    @asyncio.coroutine
    def le_teclado(self):
        while self.jogando:            
            key = yield from self.loop.run_in_executor(self.tpool, self.tela.getch)            
            if(key!=None):
                nx, ny = self.x, self.y
                if key == b'4':
                    if nx > 1:
                        nx-=1
                elif key == b'6':
                    if nx < Jogo.LARGURA*3-1:
                        nx+=1
                elif key == b'8':
                    if ny > 0:
                        ny -=1
                elif key == b'2':
                    if ny < Jogo.ALTURA*2:                        
                        ny +=1
                elif key == b"S":
                    self.fim_do_jogo()
                    break
            if self.pode_mover(nx,ny) and (nx, ny) != (self.x,self.y):
                self.x, self.y = nx, ny
                self.desenha()           

    def pode_mover(self, x,y):
        return self.labirinto[y][x]==' '

    def desenha(self):
        self.tela.set_color(*self.labirinto_cores)
        #self.tela.clear()
        self.tela.gotoXY(0,0)
        self.tela.set_title("Labirinto") 
        for linha in self.labirinto:
            print(linha)
        self.tela.gotoXY(self.x, self.y)
        self.tela.cprint(*self.jogador_carac)

    @asyncio.coroutine
    def relogio(self):
        while self.jogando:
            self.tela.print_at(10,23,datetime.datetime.now().strftime("%d/%m/%y %H:%M:%S"))
            yield from asyncio.sleep(1)

    def execute(self):
        self.tela.clear()
        self.desenha()
        try:
            asyncio.async(self.le_teclado())
            asyncio.async(self.relogio())
            self.loop.run_forever()
        except KeyboardInterrupt:
            print("exit")
        finally:
            self.tpool.shutdown()
            self.loop.close()
            self.tela.restore_buffered_mode()

jogo = Jogo()
jogo.execute()

Para adicionar um pouco de variação ao jogo, eu baixei uma função que gera labirintos deste site. O programa foi modificado para retornar uma lista de strings, que é utilizada para detectar as paredes do labirinto.

O loop de eventos é parecido com o dos artigos anteriores, mas desta vez um ThreadPool está sendo criado. Um thread extra é necessário, pois vamos bloquear a rotina até que uma tecla seja pressionada. Esta construção funciona com várias rotinas bloqueantes que não podem ser utilizadas com asyncio, pois interromperiam a execução de todas as outras corotinas.

    @asyncio.coroutine
    def le_teclado(self):
        while self.jogando:            
            key = yield from self.loop.run_in_executor(self.tpool, self.tela.getch)            
            if(key!=None):
                nx, ny = self.x, self.y
                if key == b'4':
                    if nx > 1:
                        nx-=1
                elif key == b'6':
                    if nx < Jogo.LARGURA*3-1:
                        nx+=1
                elif key == b'8':
                    if ny > 0:
                        ny -=1
                elif key == b'2':
                    if ny < Jogo.ALTURA*2:                        
                        ny +=1
                elif key == b"S":
                    self.fim_do_jogo()
                    break
            if self.pode_mover(nx,ny) and (nx, ny) != (self.x,self.y):
                self.x, self.y = nx, ny
                self.desenha()

Criamos le_teclado como uma corotina, mas como o módulo colorconsole não foi criado para trabalhar com corotinas, precisamos chamar self.tela.getch usando um Executor, no caso, nosso ThreadPool. Desta forma, nossa chamada bloqueante será executada em um thread do ThreadPool e nosso loop de eventos vai continuar a executar normalmente. Quando pressionarmos uma tecla, a função self.tela.getch vai retornar e a partir daí, trabalharemos com o resultado no yield from. O resto do método verifica se a tecla é de movimento ou de saída do jogo. No final, verificamos se o jogador se mexeu ou se a nova posição seria a de uma parede. Caso a posição seja alterada, redesenhamos a tela, para que a nova posição do jogador seja visível.

    @asyncio.coroutine
    def relogio(self):
        while self.jogando:
            self.tela.print_at(10,23,datetime.datetime.now().strftime("%d/%m/%y %H:%M:%S"))
            yield from asyncio.sleep(1)

A corotina relogio exibe a data e hora atuais a cada 1 segundo na tela. Esta execução ocorre mesmo se nada pressionarmos no teclado, confirmado a correta execução de nosso loop de eventos. Este programa pode ser executado para conter inimigos e movê-los em outra corotina, similar a do relógio.

Testado no Windows 8.1 com Python 3.4.1, colorconsole 0.7.1.