segunda-feira, 7 de dezembro de 2009

Problema 8.13

Para animar o nosso programa uma possibilidade simples é usar o módulo cTurtle. Vamos definir o nosso boneco de acordo com as coordenadas dadas na figura.



O boneco estará separado em 6 partes: cabeça, tronco, braços e pernas. Isto coloca um limite de 6 tentativas falhadas. Claro que podemos alterar. Por exemplo, é preciso falhar 2 vezes seguidas para mais uma parte do corpo ficar pendurada. De qualquer maneira o que temos que introduzir no nosso código é uma chamada à operação de desenho cada vez que falhamos. Se é a nossa i tentativa falhada, então desenhamos a parte i do corpo. Eis o código.

import cTurtle

def desenha_enforcado(i):
cTurtle.up()
if(i==0):
cTurtle.goto(60,-90)
cTurtle.setheading(180)
cTurtle.down()
cTurtle.forward(130)
cTurtle.right(90)
cTurtle.forward(160)
cTurtle.right(90)
cTurtle.forward(70)
cTurtle.right(90)
cTurtle.forward(20)
cTurtle.up()
cTurtle.goto(-70,50)
cTurtle.down()
cTurtle.goto(-50,70)
elif(i==1):
cTurtle.goto(-20,30)
cTurtle.down()
cTurtle.circle(20)
elif(i==2):
cTurtle.goto(0,10)
cTurtle.down()
cTurtle.goto(0,-30)
elif(i==3):
cTurtle.goto(0,0)
cTurtle.down()
cTurtle.goto(-20,-10)
elif(i==4):
cTurtle.goto(0,0)
cTurtle.down()
cTurtle.goto(20,-10)
elif(i==5):
cTurtle.goto(0,-30)
cTurtle.down()
cTurtle.goto(-20,-70)
cTurtle.goto(-30,-70)
elif(i==6):
cTurtle.goto(0,-30)
cTurtle.down()
cTurtle.goto(20,-70)
cTurtle.goto(30,-70)
cTurtle.hideturtle()

Problema 8.11

A tudo o que já foi feito anteriormente junta-se agora a introdução do conceito de nível. O nível de um jogador (iniciado, normal e perito, vai determinar a complexidade da palavra a adivinhar e o número de tentativas a que tem direito. No nosso caso, escolhemos o seguinte:

(1) iniciado: palavras até 4 caracteres, tentativas igual à soma de 6 com o comprimento da palavra;
(2) normal: palavras até 8 caracteres, tentativas igual à soma de 6 com o número de caracteres diferentes na palavra;
(3) perito: palavra com mais do que 8 caracteres, tentativas igual à soma de 6 com 0.8 * número de letras diferentes.

Posto isto apresentemos as definições que tratam da escolha da palavra (do ficheiro onde estão as palavra do nível escolhido), e do número de tentativas permitido.


def escolhe_nivel():
""" Escolhe o nível. Implicações para as palavras e o número de tentativas."""
print
print "Níveis possíveis: Iniciado, Normal, Perito."
nivel = raw_input('Que nível [I/N/P]? ')
if nivel == 'I':
return '/tempo/data/pal_iniciado.txt', 'I'
elif nivel == 'N':
return '/tempo/data/pal_normal.txt', 'N'
else:
return '/tempo/data/pal_perito.txt', 'P'

def define_tentativas(dicio_pal, nivel):
diferentes = len(dicio_pal)
comprimento = sum([ len(indices) for indices in dicio_pal.values()])
if nivel == 'I':
return 6 + comprimento
elif nivel == 'N':
return 6 + diferentes
else:
return int(6 + 0.8 * diferentes)

No programa que joga precisamos introduzir a parte em que se define o nome do ficheiro onde vamos buscar a palavra, e o número de tentativas. O resto é mais do mesmo. O leitor deve consultar as soluções aos problemas anteriores.

def hang811():
while True:
# Definição do nível
ficheiro,nivel= escolhe_nivel()
# --- palavra secreta
palavras = open(ficheiro).read().split()
secreta = list(random.choice(palavras))
dicio = seq_to_dict(secreta)
# --- parâmetros
tentativas = define_tentativas(dicio,nivel)
acertou = False
estado = cria_estado(list('_'* len(secreta)), [],tentativas)
# resto igual
.......

Problema 8.10

É-nos pedido uma alteração ao programa de base (problema 8.7), por forma a ser possível ter informação estatística. Vamos discutir o princípio, e não vamos estar preocupados com definir informação detalhada do jogador e seu desempenho. Assim ,o que vamos querer sobretudo é que a informação sobreviva, mesmo depois de o jogador ter terminado de jogar. Isso significa ter a informação armazenada externamente, logo significa usar ficheiros. No entanto, pode ser conveniente internamente ao programa usar outra organização. Como vamos ter vários jogadores, cada um deles com informação estatística associada, o uso de dicionários surge como a forma mais natural de guardar a informação. Para tudo isto ser possível teremos que definir duas operações de interface: uma que converte do ficheiro para o dicionário e, uma segunda, que faz o inverso (nota para o programador savvy: também pode usar o módulo pickle e, neste caso não é preciso preocupar-se com as conversões!). São essas definições que apresentamos de seguida.

def fich_to_dict(ficheiro):
"""Transforma os dados de um ficheiro para um dicionário."""
linhas = ficheiro.readlines()
dicio = {}
for linha in linhas:
nome,jogos,vitorias = linha.split()
dicio.update({nome:[int(jogos),int(vitorias)]})
return dicio

def dict_to_fich(dicio, ficheiro):
"""Transforma os dados de um dicionário para um ficheiro."""
f_out= open(ficheiro,'w')
for chave,valor in dicio.items():
linha = chave + '\t\t' + str(dicio[chave][0]) + '\t' + str(dicio[chave][1]) + '\n'
f_out.write(linha)
f_out.close()
return dicio

Por outro lado, a parte inicial do programa também deve ser alterada, de modo a identificar o jogador e saber como actualizar os dados no final de cada jogo. Vejamos como.

def hang810a(fich_dados):
# Mensagem inicial
print 'Bem Vindo ao Jogo do Enforcado!!!'
print 'Vamos jogar!'
# recupera informação estatística
f_in = open(fich_dados,'r')
dicio_jogadores = fich_to_dict(f_in)
f_in.close()
# identificação do jogador
nome = raw_input('O seu nome por favor: ')
if nome in dicio_jogadores:
print 'Olá de novo!'
print 'O seu desempenho actual é:'
print 'Jogos efectuados: %d \nVitórias: %d' % (dicio_jogadores[nome][0], dicio_jogadores[nome][1])
else:
print 'Bem Vindo pela primeira vez!'
dicio_jogadores[nome]=[0,0]

# jogar
jogar = True
while jogar:
hang810(nome,dicio_jogadores)
jogar = continuar(jogar,dicio_jogadores, fich_dados)


def continuar(jogar,dicio_jogadores,fich_dados):
# Jogar mais??
mais = raw_input('mais? [S/N]: ')
while mais not in ['S','N', 's','n','sim','não']:
mais = raw_input('A resposta tem que ser [S/N]. A sua resposta: ')
if mais in ['N','n','não']:
# Actualiza estatística
dict_to_fich(dicio_jogadores,fich_dados)
print 'Adeus, até à vista...'
jogar = False
return jogar

Como se pode ver o programa depois de fazer a inicialização necessária, chama o programa que efectivamente joga. Aqui tivemos que fazer as alterações que envolvem a actualização da parte estatística no final de cada jogo. Por outro lado, mantivemos a parte do programa que permite jogar mais do que uma vez.


def hang810(nome,dicio_jogadores):

# --- palavra secreta
palavras = open('/tempo/data/palavras.txt','r').read().split()
secreta = list(random.choice(palavras))
dicio = seq_to_dict(secreta)

# --- parâmetros
tentativas = len(secreta)
acertou = False
estado = cria_estado(list('_'* len(secreta)), [],tentativas)

# Começa o jogo
for tentativa in range(tentativas):
# Estado actual
mostra_estado(estado)
# joga
letra = adivinha(estado)
# analisa resposta
if letra in dicio:
# --- Acertou na letra!
indices = dicio[letra]
pal_utilizador = get_palavra(estado)
for ind in indices:
pal_utilizador[ind] = letra
estado= set_palavra(estado,pal_utilizador)
# --- Acertou na palavra??
if fim(secreta,pal_utilizador):
acertou = True
mensagem_sim(secreta)
break
# Actualiza estado
estado = actualiza_estado(estado, estado['palavra'],letra, get_tentativas(estado) - 1)
# actualiza estatística
dicio_jogadores[nome][0] = dicio_jogadores[nome][0] + 1
if acertou:
dicio_jogadores[nome][1] = dicio_jogadores[nome][1] + 1
# mensagem final
mensagem_fim(acertou,secreta)

Falta apenas incluir as definições auxiliares introduzidas nas versões anteriores.

domingo, 6 de dezembro de 2009

Objectos com Classe!!

Temos estado a discutir como a definição de uma estrutura de dados adequada pode ser benéfica para a qualidade da programação. A estrutura de dados tem duas componentes: o objecto propriamente dito, devidamente representado, e um conjunto de operações sobre o objecto, operações que dependem da representação escolhida. Dizemos que as operações escondem o objecto do exterior, ou, dito de outro modo, o objecto fica encapsulado pelas operações. Normalmente o programador deve-se preocupar em definir estrutura de dados que sejam eficientes computacionalmente, passando, se for esse o caso, a só ter que se preocupar com as funcionalidades oferecidas pela estrutura. O leitor mais atento já ligou este aspecto ao que se passa com os tipos de dados das linguagens de programação: nós operamos com números inteiros, por exemplo, sem cuidar de saber como é que estes inteiros estão organizados. Apenas nos interessam as funcionalidades. Alguém teve o trabalho de definir esses tipos para nós. Mas será que nós podemos definir novos tipos de dados com as mesmas características de abstracção? Já vimos que sim! Mas podemos fazer, diferente, para melhor? Uma vez mais a resposta é sim, e liga-se ao conceito de objecto tal como é usado em programação orientada aos objectos: o objecto é um todo englobando as duas componentes acima referidas.


Neste paradigma de programação o conceito de classe é central. De um modo simples, uma classe é um modelo abstracto de objectos. Traduz as características comuns a todos os objectos da classe.Na vida comum nós falamos da classe dos mamíferos, sendo o meu cão ‘Bobi’ e eu objectos dessa classe. Dizemos que os objectos são instâncias da classe. Repetindo: os objectos formam um todo com as suas características (estado) definidas por meio de atributos (por exemplo, palavra, letras usadas, tentativas) e as operações (por exemplo, add_letra_pal) determinando o seu comportamento.


Em Python todos os tipos de dados são implementados como classes. Vamos tentar fazer o mesmo com o conceito de estado apresentado no problema 8.9.

# classe Estado
class Estado(object):

# Construtor
def __init__(self,palavra =[],usadas=[],tentativas=0):
""" Cria estado 'vazio'."""
self.palavra = list(palavra)
self.usadas = list(usadas)
self.tentativas = tentativas

# Acessores
def get_estado(self):
# Devolve estado
return (self.palavra, self.usadas, self.tentativas)

def get_pal(self):
return self.palavra

def get_letras(self):
return self.usadas

def get_tentativas(self):
return self.tentativas

# Modificadores
def set_estado(self, palavra,letras,tentativas):
# Altera estado
self.palavra = list(palavra)
self.usadas = list(letras)
self.tentativas = tentativas


def set_palavra(self,pal):
self.palavra = list(pal)


def set_usadas(self,letras):
self.usadas = list(letras)


def set_tentativas(self,tenta):
if tenta >= 0:
self.tentativas = tenta
else:
print 'O valor não pode ser negativo. Foi indicado %d.' % tenta


def add_letra_palavra(self, letra,l_pos):
""" Junta a letra em todas as posições não ocupadas indicadas em l_pos."""
for ind in l_pos:
if self.palavra[ind] == '_':
self.palavra[ind] = letra
else:
print 'Posição %d já ocupada. Estado inalterado!' % ind


def add_letra_usadas(self, letra):
""" Junta a letra às letras usadas."""
if not letra in self.usadas:
self.usadas.append(letra)
else:
print '%s já existente. Estado inalterado!' % letra

def add_tentativas(self,tenta):
""" Modifica o valor das tentivas em tenta."""
novo_valor = self.tentativas + tenta
if novo_valor >= 0:
self.tentativas = novo_valor
else:
print 'O valor não pode ser negativo. o Resultado foi %d.' % novo_valor


# Auxiliares
def __str__(self):
palavra = ' '.join(self.palavra)
usadas = ', '.join(self.usadas)
tentativas = self.tentativas
return 'Palavra: %s\nUsadas: %s\nTentativas: %d' % (palavra,usadas,tentativas)

if __name__ == '__main__':
estado = Estado()
estado.set_palavra('__n_n_')
estado.add_letra_usadas('b')
estado.set_tentativas(7)
print estado
estado.add_letra_usadas('b')
estado.set_tentativas(-4)
estado.add_letra_palavra('a',[1,2,3,5])
estado.add_letra_usadas('a')
print estado


Carregue o código e execute-o. Sem entrar em muitos detalhes, o que podemos dizer por comparação ao que fizemos anteriormente?

(1) Aparece uma nova construção denominada classe;

(2) As definições anteriormente apresetnadas estão agora no interior da classe;

(3) O construtor tem agora o nome __init__ (trata-se de um método especial que permite ao utilizador fugir ao construtor por defeito);

(4) Já não temos um dicionários mas sim atributos do objecto definidos no interior do método __init__;

(5) O nome da estrutura (estado) é substituído pelo nome genérico self;

(6) Os modificadores alteram os atributos e não devolvem por return;

(7) A chamada dos métodos da classe usa a notação por ponto (dot notation);

(8) Na chamada dos métodos não se usa o parâmetro self


Muito mais poderia ser dito, mas ficamos por aqui. O leitor interessado pode alterar o código do enforcado para usar a nova classe.

Estruturas de Dados e Abstracção

Regressemos à ideia de estado avançada no problema do Enforcado. Dissemos que o estado era o que andava a ser comunicado entre o programa e o utilizador. Dissemos também que a implementação seria feita com um dicionário. Existem algumas questões interessantes que se colocam. Uma primeira, prende-se com de saber como definir uma camada de abstracção entre a estrutura de dados estado e o resto do mundo. Uma segunda, é a de saber como reagir a uma modificação na implementação do estado, por exemplo, passando de dicionário para lista.


Para responder a estas questões vamos começar por definir uma estrutura de dados, a que chamaremos estado, e que, como referimos será implementada como um dicionário.

# estado como dicionário
# {'palavra':..., 'usadas':..., 'tentativas':...}
# palavra e usadas são listas de caracteres. tentativas é um inteiro não negativo

# Construtor
def cria_estado():
""" Cria estado 'vazio'."""
estado= dict(palavra=[],usadas=[],tentativas=0)
return estado

# Acessores
def get_estado(estado):
# Devolve estado
pal = ' '.join(estado['palavra'])
letras = ', '.join(estado['usadas'])
tenta = estado['tentativas']
return (pal, letra,tenta)

def get_pal(estado):
return ' '.join(estado['palavra'])

def get_letras(estado):
return ' '.join(estado['usadas'])

def get_tentativas(estado):
return estado['tentativas']

# Modificadores
def set_estado(estado, palavra,letras,tentativas):
# Altera estado
estado['palavra'] = list(palavra)
estado['usadas'] = list(letras)
estado['tentativas'] = tentativas
return estado

def set_pal(estado,pal):
estado['palavra'] = list(pal)
return estado

def set_letras(estado,letras):
estado['usadas'] = list(letras)
return estado

def set_tentativas(estado,tenta):
if tenta >= 0:
estado['tentativas'] = tenta
else:
print 'O valor não pode ser negativo. Foi indicado %d.' % tenta
return estado

def add_letra_pal(estado, letra,l_pos):
""" Junta a letra em todas as posições não ocupadas indicadas em l_pos."""
for ind in l_pos:
if estado['palavra'][ind] == '_':
estado['palavra'][ind] = letra
else:
print 'Posição %d já ocupada. Estado inalterado!' % ind
return estado

def add_letra_letras(estado, letra):
""" Junta a letra às letras usadas."""
if not letra in estado['usadas']:
estado['usadas'].append(letra)
else:
print '%s já existente. Estado inalterado!' % letra
return estado

def add_tentativas(estado,tenta):
""" Modifica o valor das tentivas em tenta."""
novo_valor = estado['tentativas'] + tenta
if novo_valor >= 0:
estado['tentativas'] = novo_valor
else:
print 'O valor não pode ser negativo. o Resultado foi %d.' % novo_valor
return estado

# Auxiliares
def mostra_estado(estado):
# Mostra palavra
print 'Palavra Actual: ',' '.join(estado['palavra'])
print
# Mostra letras usadas
print 'Letras já usadas: ',', '.join(estado['usadas'])
print
# Mostra tentativas restantes
print 'Ainda tem as tentativas: ', estado['tentativas']
print

Acabamos de mostrar como uma estrutura de dados se define através de grupos de operações. Um (ou mais) construtor, acessores (para o todo ou a parte dos elementos da estrutura), modificadores (do todo ou da parte da estrutura) e, ainda operações auxiliares. Algumas dessas operações são simétricas: uma consulta (get), a outra altera (set ou add). Com base nelas o nosso código para o enforcado pode ser reescrito.


def hang89():
# inicialização
# --- palavra secreta
palavras = open('/tempo/data/palavras.txt').read().split()
secreta = list(random.choice(palavras))
dicio = seq_to_dict(secreta)
# --- parâmetros
TAMANHO = len(secreta)
LIMITE = limite(TAMANHO)
estado = cria_estado()
estado = set_estado(estado,'_'* TAMANHO,'',LIMITE)
acertou = False
# Entra no jogo
for tentativa in range(LIMITE):
# estado actual
mostra_estado(estado)
# joga
letra = adivinha(estado['usadas'])
# analisa resposta
if letra in dicio:
# --- Acertou na letra!
indices = dicio[letra]
estado = add_letra_pal(estado,letra,indices)
# --- Acertou na palavra??
if fim(secreta,estado['palavra']):
acertou = True
mensagem_sim(secreta)
break
# actualiza estado
esatdo = add_letra_letras(estado,letra)
estado = add_tentativas(estado, -1)
# mensagem final
mensagem_fim(acertou,secreta)

A pergunta natural que se coloca é a de saber o que ganhámos com estas alterações. A resposta liga-se às duas questões iniciais. Imaginemos que decidimos altera a representação de um dicionário para uma lista. Como proceder? Nesta abordagem basta alterar as operações sobre a estrutura de dados estado. Não precisamos alterar nada no código do programa principal! Programar por camadas de abstracção tem elevados ganhos de produtividade.

Problema 8.9

No jogo do Enforcado existe um conceito importante que é o conceito de estado, conceito esse que traduz a situação do jogo num dado momento. Jogar significa provocar uma mudança de estado. Um outro aspecto neste jogo é o da comunicação entre o programa e o utilizador. Para que essa comunicação seja efectiva deve ser implementado um bom interface entre ambos. É o que vamos mostrar usando precisamente a noção de estado. Isso vai-nos levar a alterar o programa hang87(), mostrando que a nossa análise inicial não foi a melhor. Concentrar tudo no estado vai ser benéfico, permitindo mais tarde alterar o programa de modo mais fácil, caso a especificação seja alterada.


Para já decidimos incluir no estado a palavra que o utilizador está a construir, as letras já tentadas e o número de tentativas de que ainda dispõe. Não é difícil aceitar que a melhor estrutura para guardar esta informação será um dicionário. As operações sobre o estado serão as operações de consulta e de alteração de componentes. Apresentamos a nova versão de hang87.

def hang89():
# inicialização
# --- palavra secreta
palavras = open('/tempo/data/palavras.txt').read().split()
secreta = list(random.choice(palavras))
dicio = seq_to_dict(secreta)
# --- parâmetros
TAMANHO = len(secreta)
LIMITE = limite(TAMANHO)

estado = {'palavra':list('_'* TAMANHO),'usadas':[],'tentativas':LIMITE}
acertou = False
# Entra no jogo
for tentativa in range(LIMITE):
# estado actual
mostra_estado(estado)
# joga
letra = adivinha(estado['usadas'])
# analisa resposta
if letra in dicio:
# --- Acertou na letra!
indices = dicio[letra]
for ind in indices:
estado['palavra'][ind] = letra
# --- Acertou na palavra??
if fim(secreta,estado['palavra']):
acertou = True
mensagem_sim(secreta)
break
# actualiza estado
estado['usadas'] = estado['usadas'] + [letra]
estado['tentativas'] = estado['tentativas'] - 1
# mensagem final
mensagem_fim(acertou,secreta)

Problema 8.8

Num post anterior apresentámos a solução para o problema 8.7. Chamámos a função principal hang87. Com base nisso vamos mostrar a solução para este problema.


def hang88():
print 'Vamos jogar!'
jogar = True
while jogar:
hang87()
mais= raw_input('Jogar mais? [S/N]: ')
if mais == 'N':
print 'Adeus, até à vista...'
jogar = False

Como se pode ver não alterámos o nosso programa principal. Apenas definimos um programa envolvente (os ingleses chamam-lhe wrapper). Claro que esta solução é muito simplista, e pressupõe que tudo o que não for N significa que se quer continuar a jogar. Mas podemos melhorar a solução.

def hang88b():
print 'Vamos jogar!'
jogar = True
while jogar:
jogar = continuar(jogar)

def continuar(jogar):
mais = raw_input('mais? [S/N]: ')
while mais not in ['S','N', 's','n','sim','não']:
mais = raw_input('A resposta tem que ser [S/N]. A sua resposta: ')
if mais in ['N','n','não']:
print 'Adeus, até à vista...'
jogar = False
return jogar


Deixamos ao leitor a tarefa de fazer à sua maneira. A moral desta solução era apenas a de que não foi preciso alterar o nosso programa principal.


Ao longo da vida de um programador a tarefa mais comum é ter que manter um programa, seja devido a erros seja devido a alterações na especificação. Quanto melhor tiver sido construído o nosso programa, mais fácil serão estas tarefas!