Objectivos

  1. Concepção de classes em Java que integram alguns conceitos fundamentais da Programação com Objectos, como a herança, substituição de métodos e polimorfismo.
  2. Utilização das ferramentas que permitem editar (vi, emacs, etc.), compilar (javac) e executar (java) programas Java.
  3. Realização de comentários utilizando a ferramenta javadoc.
  4. Utilização de packages em Java.

Actividade

  1. Realização de comentários a código Java utilizando o formato da ferramenta Javadoc: ver exemplo.
  2. Resolução do enunciado da aula. O código realizado servirá como base para o 5ª Exercício de Programação (EP5), a entregar até ao início da próxima aula de laboratório.

Comentários em Java

Javadoc é um programa fornecido pelo ambiente de execução do Java que é responsável por gerar documentação sobre código Java.  A documentação gerada está no formato HTML O formato de comentários utilizado por Javadoc tornou-se no de facto standard para documentar classe Java. Mais informação na página oficial do Javadoc

Enunciado da Aula

Aplicando os conceitos OO que já conhece, concretize as classes necessárias para representar a funcionalidade correspondente ao Jogo do Galo. Pode ser necessária a criação de classes adicionais não descritas abaixo. 

O Jogo do Galo tem duas entidades principais: JogoGalo e Peça. A classe JogoGalo mantém a informação sobre o estado actual do jogo (ou seja as várias peças já jogadas pelos dois jogadores) e é responsável por verificar se uma dada jogada é válida e se o jogo já chegou ao fim.  A classe Peca representa uma jogada realizada por um jogador. De seguida descrevem-se estas duas entidades com um detalhe maior. As várias classes desenvolvidas para este trabalho devem ser colocadas no package jogogalo. Fundamentalmente, os packages em Java são uma forma de organizar o código, permitindo agrupar tipos relacionados entre si. A seguinte secção fornece mais informação sobre o conceito de package em Java.


A Entidade Peça


A classe Peca representa uma peça jogada por um jogador e deve ter a seguinte funcionalidade:

  • Devolver o símbolo (representado por uma cadeia de caracteres) que representa a peça. O valor retornado por este método depende se a peça foi jogada pelo primeiro ou pelo segundo jogador: "x" (primeiro jogador), "o"  (segundo jogador);
  • Devolver o nome do jogador associado à peça: "Jogador 1" para uma peça pertencente ao primeiro jogador e "Jogador 2" no caso de uma peça pertencente ao segundo jogador.
  • Verificar se duas peças (a própria e a recebida como argumento) pertencem ao mesmo jogador. Este método  devolve um valor boleano. Há várias formas de concretizar este método. Uma consiste em verificar se os dois objectos em causa partilham a mesma classe. Outra é verificar se o simbolo retornado por ambas as peças em causa é o mesmo. Uma terceira hipótese é verificar se o nome do jogador associado às duas peças em causa é o mesmo.

Esta classe deverá ser abstracta e representará o conceito abstracto de peça do qual vão existir duas concretizações, PecaJogador1 e PecaJogador2, que irão representar as peças do primeiro e do segundo jogador, respectivamente. Parte dos métodos da classe Peca são abstractos enquanto que os restantes podem já ser concretizados dado que têm um comportamento semelhante nas duas subclasses concretas. Estas duas classes deverão substituir os métodos abstractos definidos em Peca por forma a ter-se o comportamento específico desejado em cada caso.

Adicionalmente, por forma a tornar o código mais legível, pode considerar um terceiro tipo especial de Peca, designada por PecaLivre,  que representa uma posição não preenchida do jogo. A existência desta classe permite que o programador não tenha que se preocupar quando acede a uma dada posição do jogo se tem uma referência para uma peça já jogada ou contém a referência nula (null). As posições não preenchidas do jogo passariam a ter uma referência para uma instância de PecaLivre em vez da referência nula. Assim, sempre que seja criado um jogo do galo, deve ser colocada uma instância da classe PecaLivre em cada posição válida do jogo. O símbolo associado a este novo tipo seria " ", o nome do jogador poderia ser "Ninguém". Relativamente à verificação de partilha do mesmo jogador, neste caso, o método deve sempre devolver falso dado que este tipo não representa uma peça jogada por um jogador. A existência desta classe obriga a adicionar um método extra em Peca que tem como objectivo indicar se a peça em causa corresponde a uma peça livre ou corresponde a uma peça já jogada por um jogador. O comportamento por omissão deste método, devolver false, deve ser definido em Peca. Este método deve ser depois substituído em PecaLivre por forma a devolver true. Este método depois deve ser utilizado sempre que um jogador joga para verificar se a posição indicada pelo jogador corresponde a uma posição com uma peça livre (e que corresponde a uma posição ainda não preenchida  ou não. A jogada deve ser recusada no 2º caso.


A Entidade JogoGalo


Relativamente à classe JogoGalo, esta deve ter a seguinte funcionalidade/estrutura:

  • Criação de um jogo do galo com uma dada dimensão. A dimensão indicada representa o número de linhas e colunas. Assim, o número de linhas é sempre igual ao número de colunas.
  • Utilizar uma estrutura de dados que permite guardar as peças jogadas por cada jogador;
  • Adicionar uma jogada. Deve receber a peça a jogar e a posição onde a peça deve ser colocada. A posição deve ser representada por dois números inteiros que representam a linha e coluna de destino da peça. Este método deve devolver um boleano que indica se a jogada é válida ou não. Uma jogada é válida caso a posição de destino esteja livre e representa uma posição válida do jogo. Jogadas inválidas não devem alterar o estado do jogo.
  • Verificar se ainda há posições no jogo não preenchidas. Devolve um boleano;
  • Dada a última jogada realizada, verificar se algum dos jogadores ganhou o jogo. Recorde-se que o jogo é ganho quando um dos jogadores consegue ter uma linha, coluna ou diagonal preenchida só com peças deles. Este método recebe como argumento a linha e coluna da jogada realizada. O método deve primeiro aceder à peça realizada e depois verificar se as peças pertencentes à linha ou à coluna da peça jogada pertencem ao mesmo jogador da peça jogada. No caso de a posição da peça jogada pertencer a uma das duas diagonais do jogo (linha = coluna ou linha + coluna = dimensão + 1), é ainda necessário verificar se todas as peças da diagonal em causa pertencem ao mesmo jogador da peça jogada. Para verificar se duas peças pertencem ao mesmo jogador deve utilizar o método disponível na classe Peca com esta responsabilidade.
  • Mostrar estado do jogo. Este método deve devolver uma string que representa o estado do jogo.  Cada peça jogada deve ser representada por um "X" ou "0". Cada linha do jogo deve ser terminada com o caracter '\n', Cada posição deve representar a peça presente nessa posição. Posições consecutivas na mesma linha devem ser separadas pelo caracter '|'. Linhas consecutivas devem ser separadas pela cadeira de caracteres "-+-+-" (no caso de um jogo com 3 linhas e colunas). Por exemplo, um valor possível a devolver por este método para um jogo com dimensão 3 seria o seguinte:

    x| |x
    -+-+-
    x|o|o
    -+-+-
    o|x| 

Todas as classes concretizadas devem pertencer à package jogogalo.

A classe JogoGalo deve ainda definir o método estático main que deverá realizar a seguinte sequência de operações:

  1. Criar um jogo com dimensão 3.
  2. Mostrar o estado actual do jogo.
  3. Jogador 1 coloca uma peça na posição (1,1). Indicar se esta jogada ganhou o jogo ou não.
  4. Jogador 2 coloca uma peça na posição (1,2). Indicar se esta jogada ganhou o jogo ou não.
  5. Mostrar o estado actual do jogo.
  6. Jogador 1 coloca uma peça na posição (2,2). Indicar se esta jogada ganhou o jogo ou não.
  7. Jogador 2 coloca uma peça na posição (2,3). Indicar se esta jogada ganhou o jogo ou não.
  8. Mostrar o estado actual do jogo.
  9. Jogador 1 coloca uma peça na posição (3,1). Indicar se esta jogada ganhou o jogo ou não.
  10. Jogador 2 coloca uma peça na posição (1,2). Indicar se esta jogada ganhou o jogo ou não.
  11. Mostrar o estado actual do jogo.
  12. Jogador 1 coloca uma peça na posição (3,3). Indicar se esta jogada ganhou o jogo ou não.
  13. Mostrar o estado actual do jogo.
A execução deste método utilizando o comando java permitirá exercitar um pouco o código produzido. No entanto, caso o resultado escrito seja o esperado isso não quererá dizer que não existem bugs no código produzido.

Distribuição de Trabalho


Para realizar o trabalho proposto por este enunciado deve seguir a seguinte abordagem:
  • Primeiro, cada grupo deve definir com precisão as várias entidades a construir. Este passo corresponde a identificar as classes, os seus métodos e atributos.
  • Segundo, após ter definido a interface das várias classes no ponto anterior, concretize agora o esqueleto de cada uma das classes a desenvolver. O esqueleto de uma classe consiste na especificação  dos vários métodos da classe mas sem especificar a concretização de cada método.
  • Terceiro, dividir agora as várias classes pelos vários elementos do grupo. Cada elemento deverá concretizar a ou as classes que lhe foram atribuídas no seu computador. Este processo permitirá o desenvolvimento em paralelo da solução, permitindo chegar mais rapidamente à solução final.
    • Considere a seguinte versão da classe JogoGalo que apresenta uma concretização parcial desta classe sendo apenas necessário concretizar os métodos jogatemJogadasDisponiveis. Esta concretização tem dois métodos, criaSeparadorLinha e processaLinha que representam funcionalidades que apenas devem estar disponíveis a esta classe. Por esta razão, estes métodos devem ser private. Como exercício extra, concretize depois os restantes métodos desta classe.
  • Quarto, uma vez que todas as classes tenham sido concretizadas, deve-se agrupar todas as classes num único computador por forma a poder compilar e exercitar o código realizado.
Caso não consiga terminar a solução na aula de laboratório deve terminá-la posteriormente. Pode comparar a sua solução com a solução proposta pelo corpo docente.

Trabalho Extra

Após ter terminado o trabalho pedido anteriormente, crie agora uma classe chamada Test pertencente ao package test. Esta classe deverá apenas concretizar o método estático main com a funcionalidade descrita atrás.

Solução Alternativa

Neste exemplo, existem duas formas de concretizar este problema no que diz respeito à utilização de herança para distinguir o comportamento dos diferentes tipos de peças. Uma hipótese (que foi a sugerida no enunciado) é traduzir os diferentes comportamentos utilizando o mecanismo de polimorfismo. Isto corresponderá a substituir os métodos que devem ter um comportamento distinto nas diferentes classes derivadas. A segunda hipótese, que é menos genérica do que a primeira, corresponde a diferenciar o comportamento com base no estado do objecto. Por exemplo, pode-se definir um atributo na classe Peca e o método obtemSimbolo definido em Peca retorna o valor deste atributo. Agora, o constructor de Peca deverá receber um argumento o valor deste atributo. No caso da subclasse PecaJogador1, este atributo deverá ter sempre o valor "x". Para garantir isto, o constructor desta classe deverá chamar o construtor da superclasse Peca passando como argumento a cadeia de caracteres "x". Assim, já não é necessário substituir o método obtemSimbolo nas subclasses. Os outros casos podem ser abordados de forma semelhante. Estes atributos devem ter o qualificador final (porquê?).

Solução Proposta

A solução proposta só deve ser consultada após ter realizado o exercício proposto. Compare a sua solução com a solução proposta e caso haja diferenças, tente percebê-las por forma a descobrir se tem erros na sua solução ou utilizou apenas outra abordagem para realizar o trabalho pedido.