domingo, 23 de novembro de 2014

Jogo do Galo: um exercício em programação descendente

Todos conhecemos o Jogo do Galo (que os ingleses chamam de Tic-Tac-Toe). O objectivo deste post é mostrar como podemos desenvolver um programa simples para permitir um humano jogar contra o computador. Mas o nosso interesse não é tanto no jogo, mas mais em ilustrar como podemos chegar à versão final, funcional, do programa de forma segura, tomando a cada momento as decisões mínimas que nos ajudem na sua construção. Chama-se a isso Construção Descendente de Programas. Vamos a isso.

Existem muitos aspectos a considerar no problema, mas se esquecermos os detalhes podemos propor uma primeira versão do nosso programa:

01.def tic_tac_toe_0(n):
02.    # inicializa tabuleiro
03.    # mostra tabuleiro
04.    # define jogador
05.    while True : # não há vencedor ou empate
06.        if True: # humano
07.            # pede jogada
08.            # actualiza tabuleiro
09.            pass
10.        else:
11.            # define jogada
12.            # actualiza tabuleiro
13.            pass
14.        # mostra tabuleiro
15.        # Vencedor ou empate --> termina
16.        # comuta jogador
17.    # mensagem final
Como se pode ver o jogo começa essencialmente por construir um tabuleiro e definir quem é o jogador que começa. Depois entramos num ciclo em que os jogadores jogam alternadamente enquanto não houver vencedor ou o jogo estar empatado. Podemos avançar um pouco começando por definir as partes que envolvem criar um tabuleiro e mostrar o tabuleiro.
01.def tic_tac_toe_1(n):
02.    # inicializa tabuleiro
03.    tabuleiro = inicializa_tabuleiro(n)
04.    # mostra tabuleiro
05.    mostra_tabuleiro(tabuleiro)
06.    # define jogador
07.    while True : # não há vencedor ou empate
08.        if True: # humano
09.            # pede jogada
10.            # actualiza tabuleiro
11.            pass
12.        else:
13.            # define jogada
14.            # actualiza tabuleiro
15.            pass
16.        # mostra tabuleiro
17.        mostra_tabuleiro(tabuleiro)
18.        # Vencedor ou empate --> termina
19.        # comuta jogador
20.    # mensagem final
Agora precisamos definir as novas funções. Para isso, é necessário decidir a representação para o tabuleiro. Optamos por uma lista de listas. Assim podemos facilmente modificar o seu conteúdo. Cada célula do tabuleiro, um elemento da lista de listas, poderá ter um de três valores: 0, quando está vazia, ‘X’ ou ‘O’ para a marca de cada jogador.

Começando pela inicialização do tabuleiro, podemos optar pela solução trivial:
01.def inicializa_tabuleiro_0(n):
02.    tabuleiro = []
03.    for i in range(n):
04.        # linha i
05.        linha_i = []
06.        for j in range(n):
07.            linha_i += [0]
08.        tabuleiro += [linha_i]
09.    return tabuleiro
Dois ciclos. O externo trata das linhas, o interno das colunas para cada linha. Uma alternativa:
1.def inicializa_tabuleiro(n):
2.    tabuleiro = []
3.    for i in range(n):
4.        # linha i
5.        tabuleiro += [[0] * n]
6.    return tabuleiro
Quem já conhece as listas por compreensão poderia optar por:
1.def inicializa_tabuleiro(n):
2.    return [[0] * n for i in range(n)]
Agora mostrar o tabuleiro. A solução banal:
1.def mostra_tabuleiro(tabuleiro):
2.    print(tabuleiro)
Esta solução esquece a natureza bi-dimensional do tabuleiro. Mas isso pode-se resolver, ainda de modo simples:
1.def mostra_tabuleiro(tabuleiro):
2.    for linha in tabuleiro:
3.        for valor in linha:
4.            print(valor,'  ',end='')
5.        print()
E agora? Que fazer? Vamos definir o jogador:
1.def define_jogador():
2.    import random
3.    return random.choice(‘XO')
Notar onde é feita a importação do módulo. Isso tem consequências. Sabe quais são, certo? Com este modo de proceder, avançamos para uma versão mais detalhada:
01.def tic_tac_toe_2(n):
02.    # inicializa tabuleiro
03.    tabuleiro = inicializa_tabuleiro(n)
04.    # mostra tabuleiro
05.    mostra_tabuleiro(tabuleiro)
06.    # define jogador
07.    jogador = define_jogador()
08.    while True : # não há vencedor ou empate
09.        if jogador == 'X': # humano
10.            # pede jogada
11.            # actualiza tabuleiro
12.            pass
13.        else:
14.            # define jogada
15.            # actualiza tabuleiro
16.            pass
17.        # mostra tabuleiro
18.        mostra_tabuleiro(tabuleiro)
19.        # Vencedor ou empate --> termina
20.        # comuta jogador
21.        if jogador == 'X':
22.            return 'O'
23.        else:
24.            return 'X'       
25.    # mensagem final
Está na hora de resolver o problema do conceito de jogada. Vamos admitir que jogar é simplesmente indicar as coordenadas da célula onde se pretende jogar. Existe uma diferença entre ser o humano ou ser o computador. No primeiro caso o programa dever pedir ao humano essas coordenadas:
01.def pede_jogada(tabuleiro):
02.    jogada = eval(input('A sua jogada (x,y)?: '))
03.    while not possivel(tabuleiro,jogada):
04.        print('Jogada ilegal...')
05.        jogada = eval(input('A sua jogada (x,y)?: '))
06.    return jogada
07. 
08. 
09.def possivel(tabuleiro,jogada):
10.    return tabuleiro[jogada[0]][jogada[1]] == 0
Só estamos a proteger o programa contra tentavas de jogar numa célula já ocupada…. No caso do computador a jogar podemos definir definir diferentes estratégias. apresentamos duas: numa, o computador procura por uma certa ordem a primeira posição disponível; na outra, o computador escolhe aleatoriamente uma de entre as disponíveis.
01.def joga_computador(tabuleiro):
02.    """Escolha por ordem"""
03.    for i in range(len(tabuleiro)):
04.        for j in range(len(tabuleiro[0])):
05.            if tabuleiro[i][j] == 0:
06.                return i,j
07.             
08.def joga_computador_b(tabuleiro):
09.    import random
10.    """Escolha aleatória"""
11.    livres = []
12.    for i in range(len(tabuleiro)):
13.        for j in range(len(tabuleiro[0])):
14.            if tabuleiro[i][j] == 0:
15.                livres += [(i,j)]
16.    return random.choice(livres)
Daqui resulta uma nova aproximação.
01.def tic_tac_toe_3(n):
02.    # inicializa tabuleiro
03.    tabuleiro = inicializa_tabuleiro(n)
04.    # mostra tabuleiro
05.    mostra_tabuleiro(tabuleiro)
06.    # define jogador X (humano)  ou O (computador)
07.    jogador = define_jogador()
08.    while True: # não há vencedor ou empate
09.        if jogador == 'X': # humano
10.            print('Joga Humano')
11.            # pede jogada
12.            jogada = pede_jogada(tabuleiro)
13.            # actualiza tabuleiro
14.        else:
15.            print('Joga computador')
16.            # define jogada
17.            jogada = joga_computador_b(tabuleiro)
18.            # actualiza tabuleiro
19.        # mostra tabuleiro
20.        mostra_tabuleiro(tabuleiro)
21.        # Vencedor ou empate --> termina
22.        # comuta jogador
23.        jogador = comuta_jogador(jogador)
24.    # mensagem final
Com o conceito de jogada definido podemos passar à actualização do tabuleiro. Como temos que colocar marcas diferentes, a função vai ter três parâmetros: o tabuleiro, a jogada (a posição) e a marca:
1.def actualiza_tabuleiro(tabuleiro, jogada, jogador):
2.    if jogador == 'X':
3.        tabuleiro[jogada[0]][jogada[1]] = 'X'
4.    else:
5.        tabuleiro[jogada[0]][jogada[1]] = 'O'
6.    return tabuleiro
E aproximamo-nos do final:
01.def tic_tac_toe_4(n):
02.    # inicializa tabuleiro
03.    tabuleiro = inicializa_tabuleiro(n)
04.    # mostra tabuleiro
05.    mostra_tabuleiro(tabuleiro)
06.    # define jogador X (humano)  ou O (computador)
07.    jogador = define_jogador()
08.    while True: # não há vencedor ou empate
09.        if jogador == 'X': # humano
10.            print('Joga Humano')
11.            # pede jogada
12.            jogada = pede_jogada(tabuleiro)
13.            # actualiza tabuleiro
14.            tabuleiro = actualiza_tabuleiro(tabuleiro,jogada,jogador)
15.        else:
16.            print('Joga computador')
17.            # define jogada
18.            jogada = joga_computador_b(tabuleiro)
19.            # actualiza tabuleiro
20.            tabuleiro = actualiza_tabuleiro(tabuleiro,jogada,jogador)
21.        # mostra tabuleiro
22.        mostra_tabuleiro(tabuleiro)
23.        # Vencedor ou empate --> termina
24.        # comuta jogador
25.        jogador = comuta_jogador(jogador)
26.    # mensagem final
Vamos tratar agora de uma parte mais difícil: Determinar se há vencedor ou se o jogo está empatado. Comecemos pela primeira questão. Haverá vencedor se um jogador tiver N marcas consecutivas, numa linha ou numa coluna ou numa diagonal. Vamos então separar estas três verificações, parando mal uma delas nos permita tirar conclusões. Então:
01.def vencedor(tabuleiro,jogador):
02.    return vence_linhas(tabuleiro,jogador) or vence_colunas(tabuleiro, jogador) or vence_diagonais(tabuleiro, jogador)
03. 
04.def vence_linhas(tabuleiro,jogador):
05.    for i in range(len(tabuleiro)):
06.        vence = True
07.        for j in range(len(tabuleiro[0])):
08.            vence = vence and (tabuleiro[i][j] == jogador)
09.        if vence:
10.            return True
11.    return False
12.         
13.def vence_colunas(tabuleiro,jogador):
14.    for i in range(len(tabuleiro[0])):
15.        vence = True
16.        for j in range(len(tabuleiro)):
17.            vence = vence and (tabuleiro[j][i] == jogador)
18.        if vence:
19.            return True
20.    return False
21. 
22.def vence_diagonais(tabuleiro,jogador):
23.    vence_1 = True
24.    vence_2 = True
25.    # diagonal principal
26.    for i in range(len(tabuleiro)):
27.        vence_1 = vence_1 and (tabuleiro[i][i] == jogador)
28.        vence_2 = vence_2 and (tabuleiro[i][len(tabuleiro)-1-i] == jogador)
29.    return vence_1 or vence_2
A verificaão de empate é mais simples:
1.def empate(tabuleiro):
2.    for i in range(len(tabuleiro)):
3.        for j in range(len(tabuleiro[0])):
4.            if tabuleiro[i][j] == 0:
5.                return False
6.    return True
Tome atenção sobretudo ao caso das diagonais e ao modo como calculamos as respectivas posições. Para concluir, só nos falta a mensagem final. Vamos colocá-la no programa e mostar a versão final.
01.def tic_tac_toe(n):
02.    # inicializa tabuleiro
03.    tabuleiro = inicializa_tabuleiro(n)
04.    # mostra tabuleiro
05.    mostra_tabuleiro(tabuleiro)
06.    # define jogador X (humano)  ou O (computador)
07.    jogador = define_jogador()
08.    while True: # não há vencedor ou empate
09.        if jogador == 'X': # humano
10.            print('Joga Humano')
11.            # pede jogada
12.            jogada = pede_jogada(tabuleiro)
13.            # actualiza tabuleiro
14.            tabuleiro = actualiza_tabuleiro(tabuleiro,jogada,jogador)
15.        else:
16.            print('Joga computador')
17.            # define jogada
18.            jogada = joga_computador_b(tabuleiro)
19.            # actualiza tabuleiro
20.            tabuleiro = actualiza_tabuleiro(tabuleiro,jogada,jogador)
21.        # mostra tabuleiro
22.        mostra_tabuleiro(tabuleiro)
23.        # Vencedor ou empate --> termina
24.        if vencedor(tabuleiro,jogador) or empate(tabuleiro):
25.            break
26.        # comuta jogador
27.        jogador = comuta_jogador(jogador)
28.    # mensagem final
29.    print('Finito...')
30.    if empate(tabuleiro):
31.        print('Fica para o próximo jogo!')
32.    elif jogador == 'X':
33.        print('Parabéns humanóide!')
34.    else:
35.        print('Parabéns Computador')
Agora já só falta … jogar. Ou quase. Se quiser melhorar o desempenho do computador, tem que pensar numa estratégia diferente O programa seguinte mostra uma hipótese. Não quer completar??
01.def joga_computador_c(tabuleiro):
02.    if pode_ganhar_computador(tabuleiro):
03.        # joga para aí
04.        pass
05.    elif pode_ganhar_humano(tabuleiro):
06.        # bloqueia
07.        pass
08.    elif livre_centro():
09.        # joga centro
10.        pass
11.    elif livre_canto():
12.        # joga_canto
13.        pass
14.    else:
15.        # joga
16.        pass
Moral da história: devagar se vai ao longe ou a complexidade domina-se dividindo um problema em sub-problemas mais simples.

sábado, 22 de novembro de 2014

Teste 2 - TP1

P1

Que variantes da instrução de atribuição usual ( = ) conhece? Resposta:

Existem três tipos de variantes para a forma explícita da instrução de atribuição: (a) aumentada: op= que equivale a = op por exemplo: x += 5

(b) em cadeia: x = y = … = que equivale a y = ; x = y para o caso de apenas dpis nomes. Exemplo: x = y = 7

(c) multipla: x, y = , , que equivale a x = ; y = , para o caso de apenas dois nomes. Exemplo: x,y = 4,7



P2

Calcular o valor do seno de um ângulo com uma dada precisão. Apresentar ainda o número de termos usados para obter a precisão pedida. O programa deve recorrer à definição de seno que usa a série infinita.

Este problema tem então dois dados de entrada (o ângulo e a precisão) e dois de saída (o seno e o número de termos). Como nos vamos basear na precisão temos que recorrer a um ciclo while. A garantia de cumprir a precisão desejada é controlada pela diferença do valor de duas somas parciais consecutivas.
01.import math
02. 
03.def seno(x,prec):
04.    """ Cálculo do seno com uma dada precisão."""
05.    res = 0
06.    dif = 1
07.    exp = 1
08.    while dif > prec:
09.        aux = res
10.        res += (-1)**(exp -1) * (x**(2*exp - 1) / math.factorial(2**exp - 1))
11.        dif = abs(res - aux)
12.        exp += 1
13.    return res, exp
P3

Dadas duas imagens a preto e branco, representadas por um tuplo de tuplos de uns e de zeros, verifique se são o negativo uma da outra. Assegure-se que as imagens têm o mesmo tamanho, caso contrário deve ser desencadeado um erro.

Para controlar o tamanho usamos a instrução assert, que analisa se têm o mesmo número de linhas e de colunas. Depois temos dois ciclos imbricados e mal se encontrem dois valores na mesma posição das duas imagens iguais podemos abandonar pois já sabemos que não podem ser o negativo uma da outra. Lembrar que apenas existem dois valores: 0 e 1.

01.def negativo(img_1, img_2):
02.    """
03.    Verifica se duas imagens são o negativo uma da outra.Verifica as dimensões.
04.    """
05.    assert (len(img_1) == len(img_2)) and (len(img_1[0]) == len(img_2[0])), “ Erro”
06.    linhas = len(img_1)
07.    colunas = len(img_1[0])
08.    for l in range(linhas):
09.        for c in range(colunas):
10.            if img_1[l][c] == img_2[l][c]):
11.                return False
12.    return True

sexta-feira, 14 de novembro de 2014

O passeio da tartaruga

Na última aula vimos como se podia desenhar uma grelha usando turtle. Depois decidimos tornar o problema mais complexo colocando uma tartaruga a passear de modo aleatório nessa grelha. Partindo do centro da grelha a tartaruga a cada momento escolhe um de quatro movimentos (Norte, Sul, Este e Oeste). O mundo é finito pelo que a tartaruga não pode sair da grelha e isso tem que ser controlado pelo programa. Vamos ver uma solução possível.

Primeiro a grelha.

01.import turtle
02. 
03.def grelha(dim,lado):
04.    """Desenha uma grelha dim x dim em que cada célula tem de lado lado."""
05.    turtle.color("gray")
06.    tam = (dim*lado)
07.    x = -tam//2
08.    y = tam//2
09.    turtle.penup()
10.    turtle.goto(x,y)
11.    for lin in range(dim): 
12.        # Desenha linha de quadrados
13.        for col in range(dim):
14.            turtle.pendown()
15.            quadrado(lado)           
16.            turtle.penup()
17.            turtle.setx(turtle.xcor() + lado)
18.        # reposiciona
19.        turtle.penup()
20.        turtle.setposition(x, turtle.ycor()-lado)       
21.    turtle.hideturtle()
22. 
23.def quadrado(lado):
24.    for i in range(4):
25.        turtle.fd(lado)
26.        turtle.rt(90)
Notar que definimos uma função à parte para desenhar um quadrado, na posição em que a tartaruga se encontrar. Essencialmente, o programa é definido por dois ciclos. O mais exterior controla as linhas, o mais interior as colunas. Verifique também como se define a primeira posição da grela (canto superior esquerdo).

Feito isto, a parte fácil, vamos ao passeio.

01.def passeio(dim, lado, passos):   
02.    # Prepara grelha
03.    turtle.speed(0)
04.    grelha(dim,lado)
05.    turtle.color('red')
06.    turtle.home()
07.    turtle.pendown()
08.    # Passeio
09.    turtle.speed(6)
10.    turtle.dot()
11.    turtle.showturtle()
12.    lim_x = lim_y = (dim*lado)//2
13.    cor_x = 0
14.    cor_y = 0
15.    for i in range(passos):
16.        vai_para = random.choice(['N','E','S','W'])
17.        if (vai_para == 'N') and (cor_y < lim_y):
18.            cor_y += lado
19.            turtle.setheading(90)
20.            turtle.fd(lado)
21.        elif (vai_para == 'E') and (cor_x < lim_x):
22.            cor_x += lado
23.            turtle.setheading(0)
24.            turtle.fd(lado)
25.        elif (vai_para == 'S') and (cor_y > -lim_y):
26.            cor_y -= lado
27.            turtle.setheading(270)
28.            turtle.fd(lado)
29.        elif (vai_para == 'W') and (cor_x > -lim_x):
30.            cor_x -= lado
31.            turtle.setheading(180)
32.            turtle.fd(lado)
33.        else:
34.            print((vai_para,turtle.xcor(),turtle.ycor()))
35.            continue
O programa começa por desenhar (a alta velocidade!!) a grelha e coloca a tartaruga no centro da grelha. De seguida temos um ciclo que executa um número de movimentos dado como parâmetro (passos). Usamos o módulo random para poder escolher de modo aleatório o movimento. Escolhido este, temos um longo if que faz uma análise por casos. Notar que no teste controlamos se a posição escolhida está ou não dentro da grelha. No caso de o movimento não ser possível imprime uma mensagem (ramo else final.). Agora é só experimentar!