MC336 - Cilada: Especificação

Criada: 2011-08-14
Modificada: 2011-08-16 (início da especificação juiz-jogador)
Modificada: 2011-08-17 (término da especificação juiz-jogador)
Modificada: 2011-08-20 (implementação classes básicas e JSP juiz)
Modificada: 2011-08-21 (encapsulamento)
Modificada: 2011-08-25 (interfaces e mudança no Jogador)
Modificada: 2011-09-03 (juiz mais sofisticado: verifica se tem jogada)

Regras para este semestre

O jogo Cilada será usado neste semestre, mas com regras modificadas. Ao invés de ser um jogo solitário, isto é, de um jogador, será transformado num jogo entre dois jogadores, da seguinte forma. Os jogadores se alternam colocando peças sobre o tabuleiro, e aquele que primeiro chegar a uma situação em que não tiver onde colocar nenhuma das peças ainda livres, perde. Se todas as peças forem validamente colocadas no tabuleiro, a partida será considerada empatada.

Especificação do jogo

Cada jogo será mediado por um Juiz, escrito pelo instrutor, e terá dois contendores, que chamaremos aqui de Jogador 1 e Jogador 2. O Juiz dará uma ordem de inicialização para ambos os Jogadores, através do método inicializar. O único parâmetro deste método é uma Posicao, que conterá informações sobre o tabuleiro e peças a serem usados no jogo. Haverá um limite de tempo grande para a inicialização.

Após isto, o Juiz continuamente chamará o método resposta, alternadamente do Jogador 1 e do Jogador 2, passando para cada Jogador a última jogada feita e recebendo a resposta do Jogador em questão. Há uma diferença na primeira chamada a resposta, feita ao primeiro Jogador, quando o parâmetro lance passado será igual a null. Cada lance será analisado pelo Juiz para ver se é válido antes de ser enviado pelo Juiz ao outro Jogador. Isto se repetirá até a última jogada. Haverá um limite de tempo de 1 segundo para cada jogada.

sequência de jogo

Interface entre os Jogadores e o Juiz

Cada aluno deverá implementar a interface Jogador:

Jogador
void inicializar (Posicao posicaoInicial)
Jogada resposta (Jogada lance)

As interfaces Jogada e Posicao, usadas na interface Jogador, farão parte de uma base comum que será usada tanto pelos Jogadores como pelo Juiz. Uma Posicao tem duas componentes: um Tabuleiro e uma lista de Pecas. Para a lista, será usada a interface List oferecida pelo pacote java.util.

Posicao
Tabuleiro getTabuleiro()
List<Peca> getPecas()

A interface de um Tabuleiro fornece o número de linhas, o número de colunas e o tipo de cada casa, dado por um método chamado tipo que recebe uma Casa e retorna seu tipo.

Tabuleiro
int getLinhas()
int getColunas()
TipoCasa tipo (Casa casa)

A indexação das casas e o significado da rotação (veja abaixo) é dada pela figura a seguir.

tabuleiro e rotação

O TipoCasa é um enumerado com três valores: BOLA, QUADRO e CRUZ, representados em impressão pelos caracteres 'o', '#' e '+', respectivamente.

Uma Casa é simplesmente um par de coordenadas: linha e coluna.

Casa
int getLinha()
int getColuna()

A lista de peças é formada por Pecas. Há dois tipos de Pecas: peças de duas partes e peças de três partes. Para acomodar ambos os tipos, a interface Peca define três métodos, que retornam os tipos das três partes, caso a peça tenha três partes, ou de duas partes com o terceiro método retornando null, caso a peça tenha apenas duas partes.

Peca
TipoCasa getA()
TipoCasa getB()
TipoCasa getC()

Para peças de duas partes, é indiferente qual parte é chamada de a ou b. Desta forma, uma peça com tipos a='o', b='+' é equivalente a uma peça com a='+', b='o'.

Porém, o mesmo NÃO ocorre com peças de três partes. É importante observar que as partes a, b e c de uma peça com três partes referem-se às posições mostradas na figura a seguir. Note que toda peça de três partes pode, através de uma rotação, ser colocada na posição mostrada na figura. Lembramos que no jogo não é permitido "virar" uma peça, ou seja, refleti-la geometricamente. Assim, uma peça com tipos a='o', b='#', c='+' não pode ocupar o lugar de uma peça com a='+', b='#', c='o', e vice-versa. Só há uma forma de atribuir os tipos a, b e c a uma peça de três partes.

peça de 3 partes

Por fim, uma Jogada fornece uma peça e um posicionamento para ela. O posicionamento é definido por uma Casa, que identifica a posição ocupada pela parte "a" da peça (seja ela de duas ou três partes), e por um inteiro, que assume valores 0, 1, 2 ou 3, e que define uma rotação, em múltiplos de 90 graus, no sentido anti-horário, a partir do ângulo zero na convenção da trigonometria (ou seja, 0 é indo para leste, 1 é indo para norte, etc.). Esta rotação mostra a posição, relativa a "a", da parte "b" da peça (seja ela de duas ou três partes). Com isto, o posicionamento completo da peça fica definido sem ambiguidade.

Jogada
Peca getPeca()
Casa getParteA()
int getRotacao()

Dependências entre os tipos básicos

De posse da especificação acima, podemos nos dedicar a implementar o Juiz e os Jogadores. Chamaremos os tipos acima de tipos básicos do jogo. Observe que todos são interfaces, exceto TipoCasa, que é um tipo enumerado.

Para auxiliar o entendimento, vamos rabiscar um diagrama mostrando a dependência entre os tipos: qual tipo depende de qual outro para ser definido. Para nos ajudar neste diagrama, vamos primeiro extrair todas as dependências dos diagramas UML dos tipos. O resultado seria o seguinte.

O seguinte diagrama resume todas estas dependências.

dependências entre classes básicas

Note que o diagrama de dependência nos ajuda, ditando a ordem em que os tipos devem ser definidos para não gerar erro. Começamos com Casa, TipoCasa, que são tipos que não dependem de nenhum outro, e prosseguimos, sempre adicionando um tipo que dependa apenas dos que já foram definidos.

Veja aqui a implementação destes tipos básicos.

Construindo o Juiz via Jackson Structured Programming

A técnica Jackson Structured Programming (JSP) é uma ótima alternativa para construir programas pequenos ou pequenas rotinas (métodos, em Java). Trata-se de estruturar seu programa como uma árvore, onde cada nó pode ser de uma dos seguintes tipos: sequência, iteração ou seleção. Se os filhos de um nó são sequências, significa que aquele nó é decomposto na execução sequencial de seus filhos, na ordem em que aparecem. Se os filhos de um nó são seleções, significa que apenas uma das opções será executada ("if"). Se o filho de um nó é iteração, significa que o nó é um loop que repetirá o filho até que alguma condição aconteça ("while"). Os filhos de um nó devem ser todos do mesmo tipo, e nós do tipo iteração só tem um filho.

Vamos exemplificar a técnica tentando descrever a ação de um Juiz em nosso jogo de cilada. A seguir vemos um diagrama JSP descrevendo o que seria o método main do Juiz, em alto nível.

diagrama JSP para o Juiz

Agora cada caixinha deve ser programada, isto é, transformada em código (na linguagem que for; no caso, faremos em Java) . As caixas que são nós internos da árvore (não são folhas) são facilmente programáveis, uma vez que correspondem simplesmente a sequências de comandos, ou "if"s, ou "while"s. As condições dos "while"s têm que ser programadas também. Entre as folhas, teremos aquelas que são simples e podem ser transformadas em código facilmente. Se alguma folha for medianamente complexa, poderá ser também desenhada via JSP.


MC336 Home

© 2011 João Meidanis