Novo retorno… Utilizando o Gwt Window Manager
Mar 03

Eu tentei, eu juro que tentei, mas não resisti. Eu tinha que escrever um jogo. Daí resolvi começar com algo simples pra ver se GWT era uma plataforma prática para o desenvolvimento de jogos de tabuleiro para a web.

Desta forma eu resolvi implementar um jogo da velha (não consegui imaginar um jogo mais simples).

Então, antes de entrar nos detalhes da solução GWT vamos primeiro a uma explicação sobre a forma como eu implementei o jogo.

Apesar de a estrutura de dados mais óbvia para implementar o tabuleiro de um jogo da velha ser um array bi-dimensional de inteiros com dimensão 3×3, ond, por exemplo, o valor 0 representaria uma casa vazia, 1 representaria um X e 2 representaria um O. Porém, normalmente a melhor forma de implementar um jogo de tabuleiro (damas e xadrez inclusive) é utilizando bitmaps. Desta forma, um tabuleiro para um jogo da velha pode ser representado utilizando dois bitmaps, cada um com 9 bits. Um bitmap para representando o tabuleiro com as jogadas de cada jogador e cada bit representaria uma casa do tabuleiro.

Eu sei que norlamente os programadores Java não são lá muitos chegados a operações bit a bit. Mas acreditem em mim, as vezes vale a pena abrir um pouco a mente a algumas operações em baixo nível, pois elas podem ser bastante poderosas. Por exemplo, se você tiver que ordenar um array de números inteiros que não seja muito grande (que caiba na memória principal do computador) e que o maior número contido no array seja conhecido, é possível ordená-lo utilizando operações bit a bit em tempo linear, O(N), o que é bem mais rápido do que um Quicksort, O(log2(N)).

Outra vantagem da utilização das operações bit a bit é que você consegue realizar algumas operações paralelas mesmo em arquiteturas monoprocessadas. Por exemplo, em uma arquitetura de 32 bits uma palavra do processador tem 32 bits (duhhh). Então, você pode operar sobre cada palavra utilizando apenas um ciclo do processador. Ou seja, no nosso exemplo do jogo da velha, é possível comparar as 9 casas do tabuleiro ao mesmo tempo, utilizando apenas um ciclo (em contraste com um “for” de 3×3 que seria necessário caso utilizássemos um array bidimensional de inteiros).

Assim, vamos começar a programar o nosso jogo. A primeira coisa que eu fiz foi definir duas constantes inteiras para representar o jogador “X” e o jogador “O”:

[java]

public static final int CIRCLE = 0;
public static final int X = 1;

[/java]

Em seguida nós definimos os bitmaps que vão armazenar o tabuleiro (em java os operadores bit a bit trabalham com tipos inteiros, que possuem 32 bits, que no meu caso é exatamente o tamanho de uma palavra do processador do meu computador. Se você tiver um computador com 64 bits, duas operações bit a bit sobre um inteiro entrarão no pipeline do processador como sendo apenas uma, duplicando assim o nível pretendido de paralelização).

[java]

private int[] board;
[/java]

Agora eu defino as chamadas configurações de vitóra (win configurations), ou seja, disposição das peças sobre o tabuleiro que significam que um dos jogadores ganhou. Em um jogo da velha existem apenas 8 possibilidades de vitória: um jogador completando uma das três colunas, uma das três linhas ou uma das duas diagonais. Eu defini um array de inteiros contendo as 8 possíveis configurações de vitória:

[java]

private int[] winConfigurations = {

1 < < 0 | 1 << 1 | 1 << 2,
1 << 3 | 1 << 4 | 1 << 5,
1 << 0 | 1 << 3 | 1 << 6,
1 << 2 | 1 << 4 | 1 << 6,
1 << 1 | 1 << 4 | 1 << 7,
1 << 0 | 1 << 4 | 1 << 8,
1 << 2 | 1 << 5 | 1 << 8,
1 << 6 | 1 << 7 | 1 << 8};
[/java]

Eu sei que para quem não está muito habituado comeste tipo de operações o código acima pode parecer meio esquisito, mas é bem simples. Por exemplo, a linha "1 << 1 | 1 << 2 | 1 << 3" está "ligando" os bits nas posições 1, 2 e 3 do bitmap, ou seja, está definindo como preenchida a primeira linha horizontal, de cima para baixo, do tabuleiro.

Vamos agora a alguns métodos que irão definir a lógica do jogo. O primeiro método é chamado "canPlay", antes de cada jogada a ser realizada por um usuário, o jogo deve chegar se esta jogada é possível. Para isto, basta verificar se o bit nos tabuleiros correspondentes aos dois jogadores contém o valor zero:

[java]

public boolean canPlay(int index) {
return (board[CIRCLE] & (1 << index)) == 0 &&
(board[X] & (1 << index)) == 0;
}
[/java]

Após verificar se uma jogada é possível, o jogador deve efetivamente jogar. Então, para isto eu defini um método para este fim:

[java]
public void play(int player, int index) {
board[player] |= 1 << index;
}
[/java]

A lógica de funcionamento deste método é bem trivial, ele apenas "liga" o bit correspondente a posição da jogada no tabuleiro do jogador passado como parâmetro.

Por fim, é necessário escrever um método para verificar se o jogador que acabou de fazer uma jogada ganhou. Este método percorre as configurações de vitória e verifica se todos os bits 1 de alguma configuração de vitória estão presentes no tabuleiro do jogador corrente. Isto é feito da seguinte forma:

[java]
public boolean checkWin(int player) {
for (int i = 0; i < winConfigurations.length; i++) {
if ((board[player] < winConfigurations[i]) == winConfigurations[i]) {
return true;
}
}

return false;
}
[/java]

Com esses métodos a lógica do jogo da velha está completa. É possível usá-los para implementar uma versão do jogo para qualquer interface com o usuário (console, Swing, GWT, etc). Porém eu resolvi adicionar mais um método à lógica para deixar o jogo mais interessante. Eu coloquei um método responsável por adicionar uma "inteligência artificial" e possibilitar que se jogue contra o computador. Este método apenas verifica quais são as jogadas possíveis e para cada uma, verifica se caso seja feita a jogada, o jogador corrente ou o inimigo ganharão o jogo. Se houver alguma jogada deste tipo ela será selecionada, caso não haja, será retornada uma jogada aleatória da coleção de jogadas possíveis. Segue a implementação deste método:

[java]
private int chooseOnePlace() {
List possiblePlays = new ArrayList();

for (int i = 0; i < 9; i++) {
if (canPlay(i)) {
possiblePlays.add(new Integer(i));
}
}

if (possiblePlays.isEmpty()) {
return -1;
}

for (int idx = 0; idx < 2; idx++) {
int player = (currentPlayer + idx) % 2;

for (Iterator i = possiblePlays.iterator(); i.hasNext(); ) {
Integer index = (Integer)i.next();
//Place piece
board[player] |= 1 << index.intValue();

if (checkWin(player)) {
//Unplace piece
board[player] &= ~(1 << index.intValue());
return index.intValue();
}
//Unplace piece
board[player] ^= 1 << index.intValue();
}
}

return ((Integer)possiblePlays.get((int)(Math.random() * possiblePlays.size()))).intValue();
}
[/java]

Tá tudo muito bem, tá tudo muito bom, mas cadê o GWT ?!

Vamos lá. Antes de tudo, o layout do jogo será todo implementado do lado GWT. Layout este que contém a princípio apenas uma imagem adicionada ao RootPane do módulo principal.

Pause para uma pequena nota sobre uma nota "feature" do GWT 1.4, os ImageBundle. A equipe de desenvolvimento do GWT é muito preocupada com a velocidade e o tamanho dos aplicativos gerados pela tecnologia. Desta forma, quando o código javascript é gerado para uma determinada aplicação, apenas são incluídas no código final as funções da biblioteca GWT utilizadas pela aplicação. Além disto é utilizada a ofuscação como meio de compactar o tamanho do código gerado. Por fim, foi verificado que muito da banda as aplicações era gasto com a transferência de pequenas imagens que compunham a aplicação. É sabido que é mais rápido tranferir um arquivo de 1 MB do que dez arquivos de 102,4 KB. Então, os ImageBundle são uma abstração que permite ao usuário do GWT agrupar várias imagens em uma única que será transmitida apenas uma vez e, do lado cliente, quebra nas imagens constituintes.

Isto tudo é feito de forma totalmente transparente e simples para o usuário. Vejamos como definir um ImageBundle:

[java]

public interface Images extends ImageBundle {
public AbstractImagePrototype board();
public AbstractImagePrototype circle();
public AbstractImagePrototype x();
}
[/java]

É necessário apenas criar uma interface que extenda ImageBundle e que cada método da interface tenha um nome igual ao nome de uma imagem (sem a extensão), contida no diretório onde a classe principal do módulo está. As extensões permitidas são gif, jpeg e png.

Eu utilizei as seguintes imagens para representar a interface com o usuário:

board.gif

circle.gif

x.gif

 

 

 

Eu escolhi estas imagens por quê achei elas legais (e por quê achei no images.google.com). Porém ao mesmo tempo essas imagens não foram tão legais assim, por quê não dá pra escrever uma função simples que distribua as peças no tabuleiro de forma regular (por quê o desenho do tabuleiro é todo irregular). Assim, eu por tentativa-e-erro achei as posições que que as peças deveriam ficar no tabuleiro. Em seguida eu armazenei estas posições em um array:

[java]
private Point[][] positions = {{new Point(89, 60),
new Point(182, 62),
new Point(254, 67),
new Point(95, 120),
new Point(186, 125),
new Point(262, 123),
new Point(102, 205),
new Point(182, 192),
new Point(261, 191)},
{new Point(89, 55),
new Point(182, 55),
new Point(254, 57),
new Point(95, 120),
new Point(186, 120),
new Point(262, 120),
new Point(102, 195),
new Point(182, 190),
new Point(261, 190)}};
[/java]

Este é um array bi-dimensional, uma dimensão para cada jogador, contendo as posições de 0 a 8 para que cada imagem pode ocupar no tabuleiro.

A forma que eu escolhi para “desenhar” estas peças sobre a imagem do tabuleiro foi utilizando o PopupPane que vem na biblioteca do GWT. Por default os widgets GWT não possuem informação de formatação, que devem ser definidas em um arquivo CSS. Desta forma, utilizar o PopupPane sem definir nenhum estilo no GWT irá criar uma área flutuante transparente sobre o tabuleiro.

Com a parte do desenho pronta, é preciso apenas observar os eventos do mouse e identificar qual posição no tabuleiro o jogador deseja colocar uma peça. Para isto eu implementei o seguinte procedimento:

[java]
//Find the nearest position
int index = -1;
double minDistance = Double.MAX_VALUE;

Image playerImage = currentPlayer == CIRCLE ? images.circle().createImage() : images.x().createImage();
int offsetX = playerImage.getWidth() >>> 1;
int offsetY = playerImage.getHeight() >>> 1;

for (int i = 0; i < 9; i++) {
double dx = offsetX + positions[currentPlayer][i].x - DocumentUtils.getInstance().getMousePosition().x;
double dy = offsetY + positions[currentPlayer][i].y - DocumentUtils.getInstance().getMousePosition().y;
double distance = dx * dx + dy * dy;

if (distance < minDistance) {
minDistance = distance;
index = i;
}
}
[/java]

Este procedimento procura o índice do tabuleiro que esteja mais próximo da posição do mouse no ato em que o evento mouseClick foi lançado. Para isto é calculada uma distância Euclidiana simples entre a posição do mouse e o centro de cada casa do tabuleiro.

Após identificada esta posição é executado o "game loop", ou seja, os métodos da lógica são chamados na sequência correta para fazer o jogo "funcionar" :

[java]
if (canPlay(index)) {
PopupPanel imagePanel = new PopupPanel(false);
Image image = currentPlayer == CIRCLE ? images.circle().createImage() : images.x().createImage();
imagePanel.setWidget(image);
imagePanel.setPopupPosition(positions[currentPlayer][index].x, positions[currentPlayer][index].y);
imagePanel.show();

pieces.add(imagePanel);

play(currentPlayer, index);

if (checkWin(currentPlayer)) {
String playerName = currentPlayer == CIRCLE ? "circle" : "cross";
Window.alert("The player " + playerName + " wins !");
reset();
} else if (pieces.size() == 9) {
Window.alert("Draw game !");
reset();
} else {
currentPlayer = (currentPlayer + 1) % 2;
playComputer();
}
}
[/java]

O visual do jogo completo é esse:

capturaecra-wrapper-html-for-tictactoe.png
Segue o link para o download do projeto completo do jogo: tictactoe.zip

Por enquanto é só, eu tô pensando em fazer um FreeCell em GWT, caso eu faça ou invente outra coisa eu escrevo aqui.

Vlwz.

One Response to “Jogo da Velha (Tic Tac Toe)”

  1. Sávio Canuto Says:

    Brunão… bixo… tu é um torado mesmo viu… Gostaria que colocassem mais alguma coisa pra iniciantes… se tiver tempo é claro. Falow…

Leave a Reply

You must be logged in to post a comment.