quinta-feira, 31 de dezembro de 2015

Inteligência Artificial e Fragments


Olá,

Para o último post deste ano de 2015, preparei algo especial. Vamos discutir sobre como utilizar Fragments, DialogFragments e Inteligência Artificial.

A ideia é melhorar o jogo da velha que construí no post: “Jogo da velha (tic-tac-toe)” adicionando a ele as seguintes funcionalidades:

1) O usuário poderá escolher entre jogar contra outro jogador ou jogar contra o computador (modo 1 jogador e 2 jogadores);

2) Quando o jogo for executado em um aparelho na orientação retrato, deverá apresentar primeiro a tela para escolher o modo do jogo e quando o usuário clicar em “COMEÇAR” apresentar a segunda tela com o jogo na configuração escolhida;


3) Quando o jogo for executado em um aparelho na orientação paisagem, deverá apresentar na tela a escolha dos modos (1 e 2 jogadores) e o tabela do jogo, de forma que as opções fiquem no lado esquerdo ocupando um menor espaço e a tabela no lado direito, ocupando um espaço maior;

4) Ao final da partida, o jogo deverá apresentar um caixa de diálogo informando o fim do jogo e questionar ao usuário se esse deseja iniciar uma nova partida. O usuário poderá escolher clicando nos botões SIM ou NÃO.

O modo 2 jogadores já está pronto, pois foi feito no post anterior, porém, como fazer para que o jogador jogue contra o computador?

Veja o conceito de inteligência artificial dado pela Wikipédia:

Inteligência artificial (por vezes mencionada pela sigla em inglês AI - artificial intelligence) é a inteligência similar à humana exibida por mecanismos ou software.” (WIKIPÉDIA, 2015)

Então é isso que devemos fazer, ou seja, dar ao aplicativo a capacidade de analisar a situação do jogo e tomar uma decisão para executar a jogada. Vamos então listar as jogadas possíveis, na ordem que devem ser executadas pelo computador:

1) Finalizar jogo: O computador verifica que existe uma série pronta para ser fechada e a finaliza (possui 2 células marcadas e falta apenas a última para fechar);

2) Bloquear jogador: O computador verifica que o jogador está prestes a finalizar uma série e deve bloquear esta jogada;

3) Continuar: Descartadas as duas situações anteriores, o computador deve verificar se possui alguma série iniciada por ele (uma célula marcada e 2 duas livres) e marcar a próxima célula da série;

4) Atacar: A última análise que o computador deverá fazer, caso nenhuma das condições anteriores seja satisfeita, é marcar a melhor jogada, ou seja, verificar qual botão no tabuleiro que ao ser clicado, afetará a maior quantidade de séries, aumentando assim sua possibilidade de vitória. Se essa busca não retornar nenhum resultado, o computador deverá marcar a primeira célula que encontrar disponível, com o intuito de dar seguimento ao jogo.

Vamos ilustrar as jogadas do computador para facilitar o entendimento. Eu destaquei com um círculo vermelho a decisão a ser tomada (o ícone do Java representa o computador e o do Android o jogador humano):


Antes de continuar, baixe o fonte clicando aqui. Isso facilitará o entendimento.

Muito bem, todas as decisões acima apontarão para um botão, o qual deverá ser acionado efetivando assim a jogada do computador. Vamos então criar uma interface com o método getButton() para posteriormente aplicarmos o polimorfismo:

package br.com.igordev.velha.computador.jogada;

import android.widget.ImageButton;


public interface Jogada {

    ImageButton getButton();

}

Precisaremos também, adicionar à nossa classe Game 3 novos métodos:

- int getPontos(int Jogador): Retorna a quantidade de pontos do jogador na série. Este método retorna os pontos se e somente se apenas o jogador informado como parâmetro possui peças na série. Cada peça representa 1 ponto.

    public int getPontos(int jogador) {
        int inimigo = jogador == 0 ? 1 : 0;
        if (serie[jogador] > 0 && serie[inimigo] == 0)
            return serie[jogador];
        return 0;
    }

- boolean getVazio(): Retorna se a série está vazia, ou seja, não possui peça de nenhum dos jogadores.

    public boolean getVazio() {
        return (serie[0] == 0 && serie[1] == 0);
    }

- void reset(): Limpa a série atribuindo 0 (zero) pontos para ambos os jogadores.

    public void reset() {
        this.serie[0] = 0;
        this.serie[1] = 0;
    }

Agora, vamos implementar nossas jogadas. Primeiro vamos criar a classe Avanca que servirá para acionarmos 3 jogadas: Finalizar, Bloquear e Continuar.
Perceba que a classe Avanca recebe em seu construtor a lista de todos os games do jogo, o jogador a ser analisado e a quantidade de pontos buscados.

Atributos:

#jogador – Armazena o jogador a ser analisado;
#pontos – Armazena os pontos para comparação;
#games – Lista de games do jogo.

Construtor:

+Avanca(games, jogador, pontos) – Seta os atributos com os valores passados por parâmetro:

    public class Avanca implements Jogada {

    private int jogador;
    private int pontos;
    private List<Game> games;

    public Avanca(List<Game> games, int jogador, int pontos) {
        this.games = games;
        this.jogador = jogador;
        this.pontos = pontos;
    }

Métodos:

#getGame(): Busca e retorna o game em que o jogador (computador ou humano) possui a quantidade de pontos informados no construtor:

    private Game getGame() {
        for (Game game : games)
            if (game.getPontos(jogador) == pontos)
                return game;

       return null;
    }

+getButton(): Busca dentro do game retornado pelo método getGame() o botão ativo e retorna.

    @Override
    public ImageButton getButton() {
        Game game = getGame();

        if (game != null) {
            List<ImageButton> buttons = game.getImageButtons();
            for (ImageButton button : buttons)
                if (button.isEnabled()) return button;
        }
        return null;
    }

Essa classe será utilizada para direcionar as jogadas da seguinte forma:

Finalizar: Avanca(games, ID_COMPUTADOR, 2) – Procura a série em que o COMPUTADOR possui 2 peças marcadas e retorna o botão restante para finalizar a partida.

Bloquear: Avanca(games, ID_JOGADOR, 2) – Procura a série em que o JOGADOR HUMANO possui 2 peças marcadas e retorna o botão restante, permitindo que o computador bloqueie a vitória.

Continua: Avanca(games, ID_COMPUTADOR, 1) – Procura uma série do COMPUTADOR com 1 ponto (uma peça marcada) e retorna o próximo botão a ser acionado.

Caso as situações acima não retornem nenhum botão, ou seja, nenhuma delas foi satisfeita, temos que criar uma jogada que escolha o melhor botão, aquele que estatisticamente aumente probabilidade de vitória do computador. Para isso, vamos procurar o botão que aciona a maior quantidade de séries livres (vazias).

Primeiro vamos criar uma classe MelhorJodada para armazenar esta jogada. Esta classe vai setar em seu construtor: nulo para o botão e 0 (zero) para a quantidade de séries afetadas. Isso servirá para armazenar o primeiro botão e comparar com os demais botões da lista.

Atributos:

#jogos – Armazena a quantidade de séries afetadas;
#button – Armazena o melhor botão.

Construtor:

+MelhorJogada – Atribui nulo para o botão e 0 para os jogos:

    public MelhorJogada() {
        this.jogos = 0;
        this.button = null;
    }

Métodos:

+setButton(): seta o valor para o botão:

    public void setButton(ImageButton button) {
        this.button = button;
    }

+getButton(): retorna o botão armazenado

    public ImageButton getButton() {
        return  this.button;
    }

+setJogos(): seta a quantidade de séries afetadas

    public void setJogos(int jogos) {
        this.jogos = jogos;
    }

+getJogos(): retorna a quantidade de séries afetadas

    public int getJogos() {
        return jogos;
    }

Em seguida vamos implementar a jogada com a classe Ataca. Para o construtor desta classe vamos passar a lista de todos os botões do jogo:

Atributos:

#buttons – Lista de botões do jogo;

Construtor:

+Ataca(buttons) – Atribui a lista de botões:

    public Ataca(List<ImageButton> buttons) {
        this.buttons = buttons;
    }

Métodos

#getMelhorJogada(): Percorre a lista de botões analisando seus games e incrementando a variável jogos sempre que uma série vazia é encontrada. Depois de analisar o botão, compara a quantidade de séries com as do botão armazenado no objeto melhorJogada. Se o valor for maior, armazena este novo botão no lugar do botão anterior:

    private ImageButton getMelhorJogada() {
        MelhorJogada melhorJogada = new MelhorJogada();
        for (ImageButton b : buttons) {
            List<Game> games = ((Games) b.getTag()).getGames();
            int jogos = 0;
            for (Game g : games)
                if (g.getVazio())
                    ++jogos;
            if (jogos > melhorJogada.getJogos()) {
                melhorJogada.setButton(b);
                melhorJogada.setJogos(jogos);
            }
        }
        return melhorJogada.getButton();
    }

#getJogadaPossivel(): Este método é chamado caso não existam mais séries vazias. Ele é necessário para dar seguimento ao jogo, retornado o primeiro botão livre que encontrar.

    private ImageButton getJogadaPossivel() {
        for (ImageButton b : buttons)
            if (b.isEnabled()) return b;
        return null;
    }

+getButton(): Retorna o botão de acordo com o critério de análise dos métodos getMelhorJogada() e getJogadaPossivel():

    @Override
    public ImageButton getButton() {
        ImageButton b = getMelhorJogada();
        return b != null ? b : getJogadaPossivel();
    }

OK, já instanciamos nossas jogadas. Agora temos que alimentar o “cérebro” do nosso computador com uma lista de todas elas, na ordem em que serão analisadas.
Vamos criar a classe Computador e no seu método construtor informar a lista de Games e a lista de Buttons. Nesse método vamos criar também a lista de jogadas que devem ser analisadas:

Atributos:

+ID_JOGADOR – Identificado do jogador humano (0);
+ID_COMPUTADOR – Identificador do computador (1);
#jogadas – Lista de jogadas possíveis.

Construtor:

+Computador(games, buttons) – Cria a lista de jogadas na ordem em que serão analisadas:

    public Computador(List<Game> games, List<ImageButton> buttons) {
        this.jogadas.add(new Avanca(games, Computador.ID_COMPUTADOR, 2)); //finaliza
        this.jogadas.add(new Avanca(games, Computador.ID_JOGADOR, 2)); //bloqueia
        this.jogadas.add(new Avanca(games, Computador.ID_COMPUTADOR, 1)); //continua
        this.jogadas.add(new Ataca(buttons)); //ataca
    }

Métodos:

+escolhe(): Percorre a lista de jogadas para escolher qual botão acionar:

    public ImageButton escolhe() {
        ImageButton b;
        for (Jogada j : jogadas) {
            b = j.getButton();
            if (b != null) return b;
        }
        return null;
    }

Pronto, nosso aplicativo está apito a enfrentar o jogador humano, podemos então passar para o próximo item que é a montagem da tela com Fragments.

Os fragmentos passaram a existir no Android para permitir um melhor aproveitamento do espaço na tela dos tablets.

Veja abaixo um exemplo de uso, retirado da documentação oficial do Android:


Vamos fazer exatamente o que é mostrado no exemplo, começando pela criação das nossas classes que estendem Fragment. Essas classes encapsulam a lógica do nosso fragmento, permitindo que ele seja reutilizado tando na orientação retrato quanto na orientação paisagem do aparelho.

Dica Importante: O Google recomenta fortemente que sempre sejam utilizadas as bibliotecas de compatibilidade (android.support.v4.app.Fragment, android.support.v4.app.FragmentTransaction, android.support.v7.app.AppCompatActivity). Essa prática, além de manter o seu código compatível com versões anteriores do Android, evita alguns erros de inconsistência de métodos internos da interface (UI), que não executam conforme esperado.

NewGameFragmet: Tela onde o usuário escolhe o modo do jogo (1 ou 2 jogadores). Observe que, se a tela estiver na orientação retrato (ROTATION_0 ou ROTATION_180), o aplicativo chama a atividade que contém o jogo, caso contrário, apenas o reinicia, pois esse já está em exibição ao lado da tela de opções:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_new_game, container, false);

        comecar = (Button) view.findViewById(R.id.buttonComecar);
        comecar.setOnClickListener(onClickListener);

        rb1 = (RadioButton) view.findViewById(R.id.radioButton1J);

        return view;
    }

    //evento do botão COMEÇAR
    private OnClickListener onClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            Display display = getActivity().getWindowManager().getDefaultDisplay();

            int jogadores = rb1.isChecked() ? 1 : 2;

            if (display.getRotation() == Surface.ROTATION_0
                    || display.getRotation() == Surface.ROTATION_180) {
                Intent intent = new Intent(getActivity(), GameActivity.class);
                intent.putExtra("jogadores", jogadores);
                startActivity(intent);
            } else {
                GameFragment gameFragment = (GameFragment) getFragmentManager().findFragmentById(R.id.fragmentGame);
                gameFragment.resetGame(jogadores);
            }
        }
    };

GameFragment: Toda lógica do jogo, que na versão anterior ficava em MainActivity.java, foi trazida para essa classe, deixando a MainActivity apenas com o método onCreate. Esta é a função do fragmento, encapsular a lógica para que ela seja reaproveitada. Abaixo vou comentar o que foi alterado para esta nova versão.


Explanação da Classe GameFragment:


Obs.: Sempre que precisar incluir um construtor com parâmetros em um fragmento, você deverá criar um construtor padrão, mesmo que ele fique vazio. Neste momento não faz sentido explicar porquê, falo nisso novamente mais adiante.

Métodos click1Jogador e click2Jogadores:

Nesta versão, foi necessário incluir um listener diferente para quando o jogo estiver em execução no modo single player. Isso se deve ao fato de que, quando o jogador humano escolher um botão, automaticamente o aplicativo deverá comandar que o computador faça sua jogada. Veja abaixo o código do método click1Jogador:


    private OnClickListener click1Jogador = new OnClickListener() {

        @Override
        public void onClick(View v) {
            //peça jogador
            ImageButton bJogador = (ImageButton) v;
            //Incrementa a serie escolhida pelo jogador
            Game gJogador = ((Games) bJogador.getTag()).gamesAddSerie(jogador);
            atualizaStatusJogo(gJogador, bJogador);
            //peça computador
            ImageButton bComputador = computador.escolhe();
            if (bComputador != null) {
                //Incrementa a serie escolhida pelo computador;
                Game gComputador = ((Games) bComputador.getTag()).gamesAddSerie(jogador);
                atualizaStatusJogo(gComputador, bComputador);
            }
        }
    };

Tive que incluir também um construtor. Ele definirá em que modo inicial para 1 jogador.

    public GameFragment() {
        this.jogadores = 1;
    }

Parte do método onCreateView foi migrado para o método resetGame(). Nesta nova versão, o usuário poderá escolher recomeçar a partida. Veja no código do método que o objeto “computador” é criado apenas se o jogo estiver no modo single player, outro item importante, é a operação de reset dos games e a definição de null para as imagens dos botões:

    public void resetGame(int jogadores) {
        List<Game> games = listGame(l1, l2, l3, c1, c2, c3, d1, d2);
        for (Game g : games) g.reset();
        this.jogadores = jogadores;
        jogador = 0;
        jogadas = 0;
        enableButtons(true);

        OnClickListener onClickListener;
        if (jogadores == 1) {
            onClickListener = click1Jogador;
            computador = new Computador(games, listButton());
        } else
            onClickListener = click2Jogadores;

        for (int b = 0; b < 9; b++) {
            iB[b].setImageBitmap(null);
            iB[b].setBackgroundResource(R.drawable.button);
            iB[b].setOnClickListener(onClickListener);
        }
    }

No método showDialog(), foi criado um Handler para ser executado 1,1 segundos após o final do jogo. Esse handler chama um DialogFragment informando ao usuário que o jogo terminou e pergunta se ele deseja iniciar uma nova partida. Este é um exemplo de uso bem simples do DialogFragment, pois é possível criar um diálogo mais complexo, com caixa de edição e tudo... Futuramente eu monto um post só para demonstrar os tipos de diálogos.

Veja no código que eu precisei bloquear a rotação da tela antes de exibir a mensagem. Isso é necessário pois, se durante a exibição do DialogFragment o usuário girar o aparelho, causará um “Force Close Error”.

    private void showDialog() {
        lockRotation();
  //listener dos botões SIM e NÃO
        final DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

                if (which == DialogInterface.BUTTON_POSITIVE)
                    resetGame(jogadores);
                else
                    getActivity().finish();

                dialog.cancel();
                unlockRotation();
            }
        };

        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                DialogFragment resultadoJogo = new DialogFragment() {
                    @Override
                    public Dialog onCreateDialog(Bundle bundle) {
                        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                        builder.setMessage(getResources().getString(R.string.game_over));
                        builder.setPositiveButton(R.string.btn_sim, onClickListener);
                        builder.setNegativeButton(R.string.btn_nao, onClickListener);
                        return builder.create();
                    }
                };
                //força o usuário a escolher uma opção
    resultadoJogo.setCancelable(false);
                resultadoJogo.show(getFragmentManager(), "resultado");
            }
        }, 1100);
    }

Finalmente, após criar os fragmentos, vamos a montagem das telas.

Mude a visualização do seu Android Stutio para Project e crie a pasta layout-land dentro da pasta res. Nessa pasta ficará a Activity para a orientação paisagem (veja mais detalhes sobre isso no post “Utilizando as duas orientações da tela”).
Crie uma nova Activity nessa pasta com o nome activity_main.xml (configure o layout desta tela para LinearLayout horizontal). Se clicar na aba Design, vai visualizar um telefone na orientação paisagem. Adicione à tela dois fragmentos, um para a classe NewGameFragment e outro para a classe GameFragment:

Configure a propriedade weigth do NewGameFragment para 3 e a do GameFragment para 4. Essa propriedade define o quanto cada fragmento ocupará da tela, nesse caso 3 + 4 = 7, então, o fragmento da esquerda ocupará 3/7 e o da direita 4/7 da tela.

Confira abaixo, como ficou o código xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:name="br.com.igordev.velha.fragment.NewGameFragment"
        android:id="@+id/fragmentNew"
        android:layout_weight="3" />

    <fragment
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:name="br.com.igordev.velha.fragment.GameFragment"
        android:id="@+id/fragmentGame"
        android:layout_weight="4" />
</LinearLayout>

Temos que modificar também, a activity_main.xml da pasta layout. Acrescente a ela um fragmento para classe NewGameFragment.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="br.com.igordev.velha.fragment.NewGameFragment"
        android:id="@+id/fragmentNew" />
</RelativeLayout>

E por último, crie outra atividade com o nome activity_game.xml onde colocaremos um fragmento para a classe GameFragment.
Lembre-se que toda Activity deve constar no arquivo de manifesto do projeto. Se você estiver usando o Android Studio, assim que você criar uma Activity ele já a adiciona automaticamente ao manifesto.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="br.com.igordev.velha.GameActivity">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="br.com.igordev.velha.fragment.GameFragment"
        android:id="@+id/fragmentGame" />
</RelativeLayout>

Resumindo, o que acabamos de fazer foi demostrado anteriormente no desenho da documentação do Android. Criamos uma tela com 2 fragmentos (A + B) e depois uma tela para cada fragmento (A e B).
A lógica adotada pelo sistema é:
  • Sempre que o telefone estiver na orientação paisagem, instanciar e mostrar as duas classes (NewGameFragment e GameFragment);
  • Sempre que o telefone estiver na orientação retrato, instanciar e mostrar apenas a Activity com o NewGameFragment e essa por sua vez aciona a Activity que contém o GameFragment por meio do botão COMEÇAR.
Como o Android instancia essas classes automaticamente quando carrega a tela, é obrigatório  ter nelas um construtor padrão, como foi dito anteriormente.


Ufa!!! Terminamos nosso projeto. Espero que eu tenha sido claro nas explicações. Caso tenha ficado alguma dúvida, deixe aqui nos comentários que assim que possível eu respondo.

Instale o jogo no seu celular ou execute-o no emulador e veja como ficou. Existe uma técnica para enganar a inteligência artificial e ganhar. Você consegue descobrir?


Boas Festas e feliz 2016!

Nenhum comentário:

Postar um comentário