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.

Sem comentários:

Enviar um comentário