 | Escrevendo o Jogo Sudoku
Após ter instalado e configurado todas as ferramentas de pré-requisito, você pode configurar o desenvolvimento do jogo
Sudoku (consulte Download para conhecer o código fonte). Caso não esteja familiarizado
com o Sudoku, ele é um quebra-cabeça numérico baseado em lógica que se tornou absolutamente popular nos últimos anos. Ele
consiste em uma grade com nove linhas e nove colunas. O objetivo é preencher as células da grade com números de 1 a 9
de modo que cada número ocorra exatamente uma vez em cada linha, coluna e em cada uma das nove caixas 3x3. Inicialmente,
a grade é preenchida parcialmente (o suficiente para fornecer um conjunto inicial de limitadores). As células pré-preenchidas
não podem ser modificadas.
Em vez de gravar todo o código do zero, você pode utilizar uma das implementações disponíveis gratuitamente como ponto de partida. O
projeto Exemplos do Eclipse fornece uma implementação baseada no Eclipse RCP. Um ponto de partida ainda melhor é a versão
do eRCP do mesmo exemplo disponível no repositório do projeto DSDP. Embora o código por trás de um simples jogo Sudoku não
seja muito complexo, o uso de uma implementação existente permite que você direcione o foco para as tarefas relevantes para
este tutorial, como adaptar o código para execução na CLDC e utilizar a API do eSWT.
Criando o MIDlet do Sudoku
Para implementar um aplicativo Java na plataforma S60, ele deve ser construído e empacotado como um conjunto do MIDlet. O Eclipse MTJ
permite que você crie novo projetos MIDlet utilizando um simples assistente. Na perspectiva do Java ME, clique em Arquivo > Novo
> Projeto MIDlet. Insira o nome do projeto apropriado (por exemplo, sudoku) e certifique-se de que o Dispositivo de
Destino correto seja especificado (isto é, o SDK da Plataforma S60 como o SDK e o S60Emulator como o Dispositivo). A Figura 7 mostra
a página do assistente preenchida com valores de amostra. Quando você clicar em Concluir, você deverá ver o editor
Descritor de Aplicativo aberto no ambiente de trabalho.
Figura 7. Assistente Novo Projeto MIDlet
O editor Descritor de Aplicativo é utilizado para editar o arquivo JAD do projeto. Inicialmente, a única parte preenchida
com dados é a página de visão geral, conforme mostrado na Figura 8. Você pode modificar o nome do MIDlet, seu fornecedor e sua
versão, se quiser. Um clique na guia Descritor do Aplicativo no editor revela a visualização textual do arquivo JAD.
Figura 8. O Editor Descritor de Aplicativo do MIDlet
A próxima etapa lógica no desenvolvimento é criar a classe MIDlet real. No Project Explorer,
clique com o botão direito do mouse na pasta src no projeto MIDlet e escolha Novo > MIDlet do Java ME no menu de contexto. Insira
o nome do pacote desejado (por exemplo, sudoku) e especifique o nome de classe do MIDlet (por exemplo, Sudoku). A Figura 9 mostra a página do assistente preenchida. Quando você clica em Concluir, o assistente cria uma implementação do esqueleto
do MIDlet para você. Além disso, o MIDlet recém-criado é incluído no Descritor de Aplicativo. Você pode verificar isso na
página MIDlets do editor.
Figura 9. Assistente Novo MIDlet
Antes de ir direto ao eSWT, você deve gravar um código para implementar a lógica do jogo Sudoku. Utilizando o exemplo
do Sudoku do eRCP como ponto de partida, você precisa de classes que modelem o estado do jogo. Por exemplo, uma Célula representa
uma célula de grade única. Ela possui um valor atual (isto é, entre 1 e 9, inclusivo); uma célula com o valor de 0 é considerada vazia. Uma
Célula pode ser "fornecida", o que significa que ela vem pré-preenchida quando o jogo começa e não pode ser
alterada pelo usuário. Uma Célula é considerada "válida" quando a linha, a coluna e a caixa às quais ela
pertence são válidas. Por fim, uma Célula também pode fornecer uma lista de valores válidos, que são,
essencialmente, qualquer valor que ainda não esteja na linha, na coluna ou na caixa da célula.
Outras classes de modelo incluem Coluna, Linhae Caixa,
sendo que cada uma é uma representação direta do que elas significam. Uma Coluna representa nove células
verticais na grade. Ela é válida quando não contém valores duplicados. Da mesma forma, uma Linha representa
nove células horizontais na grade e é válida quando não contém valores duplicados. Uma Caixa representa uma
região de células 3x3 sem sobreposição no painel. Ela também é válida quando contém somente valores exclusivos.
A classe SudokuBoard serve de contêiner da Célula,
da Coluna e da Linha. Ela monitora Colunas,
Linhas e Caixas inválidas (o painel é válido quando nenhuma delas é inválida). Ele é
concluído quando não existem mais Células vazias. SudokuBoard também fornece
métodos para se acessar várias partes de um painel, como a Célula por seu número de coluna e linha,
bem como para determinar à qual Caixa uma Célula pertence. A classe
SudokuBoard também suporta um mecanismo notificação simples: Objetos interessados em ser notificados
sobre mudanças no painel podem se registrar no painel como listeners, contanto que implementem a interface SudokuBoardStateListener. Esse
mecanismo é útil para separar a lógica do jogo de sua UI.
Finalmente, SudokuGame serve de ponto de entrada para o modelo de jogo. Ele fornece os meios
para se criar novos painéis, bem como para se obter board factories e solucionadores disponíveis. Um SudokuBoardFactory é
uma abstração de um algoritmo gerador de painel. Da mesma forma, um SudokuBoardSolver é uma
abstração de um algoritmo solucionador do Sudoku. Uma simples implementação de um solucionador de backtracking é fornecida como gerador e solucionador. SudokuBoard também suporta um mecanismo
de notificação (através de SudokuBoardChangeListener registrado) para notificar os clientes
sobre mudanças no jogo, como, por exemplo, quando um novo painel for criado.
Ao comparar o código adaptado do exemplo do eRCP com a implementação original, você vai observar que alguns deles devem
ser refatorados para a CLDC. Especificamente, todos os usos das classes de Coleta Java tiveram que ser refatorados para
o uso de uma classe Vector muito mais simples. Da mesma forma, todos os usos de Agente Iterativo
tiveram que ser convertidos em Enumeração.
Codificando a UI com o eSWT
Até aqui, você não encontrou nenhuma referência ao eSWT. O código de exemplo mantém uma boa separação de interesses. Você já aprendeu
que todos os widgets do eSWT existem no contexto de uma exibição. O encadeamento que cria a instância Display
do singleton se torna o encadeamento de exibição, e todas as chamadas de API do eSWT devem ser feitas de dentro dele. Além de criar
widgets e controles, a tarefa do encadeamento de exibição é processar quaisquer eventos e mensagens postados nele. Quando a exibição é
descartada, o encadeamento é finalizado e o aplicativo pára.
Como o tempo de vida do MIDlet é gerenciado pela estrutura, você não pode criar sua exibição e implementar seu loop de eventos
da UI no método startApp() do MIDlet. Isso interceptaria efetivamente o encadeamento de chamada, que
pertence ao gerenciador de aplicativos, e o bloquearia até depois do encerramento do aplicativo. Além disso, seu MIDlet deve criar um
encadeamento separado e utilizá-lo como o encadeamento de exibição designado. Retornando à classe do MIDlet,
você deve criar e iniciar uma nova instância de Encadeamento na primeira vez que o método startApp() for
chamado. O método de execução do encadeamento é o lugar certo para você criar sua exibição e implementar o loop de eventos da interface com o usuário. A
Listagem 2 mostra como isso é feito.
Listagem 2. Implementação do MIDlet Inicial com Encadeamento de Exibição do eSWT e Loop de Eventos da Interface com o Usuário
public class Sudoku extends MIDlet implements Runnable {
private Thread thread;
private Display display;
private MobileShell shell;
protected void startApp() throws MIDletStateChangeException {
if (thread == null) {
thread = new Thread(this);
thread.start();
}
}
public void run() {
display = new Display();
shell = new MobileShell(display);
shell.setLayout(new FillLayout());
shell.setText("Sudoku");
shell.open();
try {
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
} finally {
display.dispose();
notifyDestroyed();
}
}
protected void pauseApp() {
// não faz nada
}
protected void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
display.asyncExec(new Runnable() {
public void run() {
if (!shell.isDisposed())
shell.close();
}
});
}
}
|
O método run do encadeamento de exibição começa criando a Exibição e seu shell de nível superior. A
classe MobileShell pode ser utilizada no lugar do shell padrão do SWT porque fornece recursos
específicos para telefone móvel, como texto de status, modo tela cheia e mudanças de borda dinâmicas. O shell recém-criado
pode ser utilizado como contêiner para todos os outros widgets e gráficos utilizados em todo o aplicativo. E o mais notável
é que a interface com o usuário do jogo é implementada na classe SudokuView, que cuida da criação de widgets necessários
na inicialização e no descarte dos recursos após a finalização. Além disso, você deve instanciar essa classe e permitir que ela crie seus
widgets chamando seu método createPartControl(Composite), passando o shell de nível superior como seu contêiner. A
Listagem 3 demonstra como fazer isso.
Listagem 3. Instanciando SudokuView e Descartando-o Quando o Shell For
Descartado
final SudokuView view = new SudokuView();
view.createPartControl(shell);
view.setFocus();
shell.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent event) {
view.dispose();
}
});
|
Com os widgets necessários, você deve fornecer ao usuário uma maneira de encerrar o jogo. A classe
Command fornece apenas esse tipo de funcionalidade; ela permite que você
anexe um listener de seleção a um dos botões padrão do telefone móvel. O botão Sair é o lugar
perfeito para o comando exit. Quando esse comando for invocado (ou seja, o botão for pressionado),
o shell de nível superior deve ser simplesmente fechado. A Listagem 4 mostra o trecho de código para
a criação do comando Exit.
Listagem 4. Criando o Comando Exit
Command exitCmd = new Command(shell, Command.EXIT, 0);
exitCmd.setText("Exit");
exitCmd.addSelectionListener(new SelectionListener() {
public void widgetDefaultSelected(SelectionEvent event) {
// não faz nada
}
public void widgetSelected(SelectionEvent event) {
shell.close();
}
});
|
Depois que tiver cuidado da criação do widget e da configuração do comando, você poderá abrir o shell e executar o
loop de eventos da interface com o usuário padrão. Contanto que o shell não seja descartado (isto é, como resultado de um fechamento), você
deverá ler e despachar eventos da interface com o usuário (chamando o método readAndDisplay()). Quando esse método
retornar false, você poderá colocar a exibição no modo suspenso (chamando sleep()), que a faz
aguardar até ser reativada por um evento recém-chegado. Quando o shell de nível superior for descartado, você deverá
descartar a exibição e sair do encadeamento.
Normalmente, o aplicativo é encerrado quando o usuário opta por fazer isso. Entretanto, quando é executado como um
MIDlet, pode ser solicitado que o aplicativo seja encerrado pelo gerenciador de aplicativos (por exemplo, em um esforço
para liberar alguns recursos, etc). Além disso, o método destroyApp(boolean) do MIDlet deve sinalizar para o encadeamento
de exibição que ele deve ser encerrado. Uma maneira de se fazer isso seria postar uma mensagem para o encadeamento de
exibição pedindo ao shell de nível superior para fechar, parando assim o loop de eventos, descartando a exibição e encerrando
o encadeamento.
Como você pode ver na Listagem 2, esse tipo de implementação de MIDlet é mais genérica e poderia suportar um aplicativo
completamente diferente, contanto que toda a construção da sua UI e o processamento de eventos do usuário estejam
encapsulados em uma classe do tipo SudokuView. De fato, a classe SudokuView
trata da criação dos widgets necessários para desenhar o painel do jogo Sudoku, criando comandos adicionais para permitir que
o usuário controle o jogo e conectando o listener do evento para tratar de eventos do teclado, bem como redimensionando e
repintando. Esse padrão é resultado da refatoração do exemplo de Sudoku do eRCP, que exibe o jogo em uma visualização do
eRCP, e não em um shell de nível superior (embora o conceito de contêiner separado seja aplicável).
Como o Sudoku tem natureza gráfica, sua interface com o usuário é desenhada através de primitivas de Graphical Context (GC), como linhas,
planos de fundo e texto. A implementação do eRCP já fornece três classes "desenhistas" — a CellDrawer para
desenhar células individuais, a BoxDrawer para desenhar caixas e BoardDrawer para
desenhar o painel do jogo Sudoku inteiro. A classe DrawingContext é usada para gerenciar
recursos gráficos, como cores e fontes utilizados pelas operações de desenho. O desenho do painel consiste em desenhar cada
uma de suas nove caixas e todas as suas 81 células. O painel também acompanha e desenha a célula em destaque — aquela com que o
usuário está interagindo no momento. Desenhar a caixa envolve o preenchimento de seu retângulo com uma cor do plano de fundo
(utilizando setBackground(Color) e fillRectangle(int, int, int, int de GC),
respectivamente).
Por fim, desenhar uma célula consiste em desenhar seu quadro utilizando drawRectangle(int, int, int, int),
bem como seu valor atual utilizando drawText(String, int, int, int). A fonte do texto é configurada através de setFont(Font) e
a cor de primeiro plano através de setForeground(Color). As células em destaque (com o mouse sobre elas) são
desenhadas com uma cor diferente e com uma linha mais grossa, e seus valores disponíveis são impressos em texto pequeno junto com o quadro
da célula. A Listagem 5 mostra o corpo do método drawCell de CellDrawer.
Listagem 5. Método drawCell de CellDrawer
Utilizando a API de GC para Desenhar a Célula
public void drawCell(GC gc, DrawingContext context) {
if (cell.isMarked()) {
gc.setBackground(context.getMarkedCellColor());
gc.fillRectangle(left, top, cellSize, cellSize);
} else if (!cell.isValid()) {
gc.setBackground(context.getInvalidCellColor());
gc.fillRectangle(left, top, cellSize, cellSize);
}
gc.setLineWidth(1);
gc.setForeground(context.getFrameColor());
gc.drawRectangle(left, top, cellSize, cellSize);
if (!cell.isEmpty()) {
if (cell.isGiven())
gc.setFont(context.getStaticCellFont(cellSize / 3));
else
gc.setFont(context.getCellFont(cellSize / 3));
String text = String.valueOf(cell.getValue());
Point textExtent = gc.textExtent(text);
try {
gc.drawText(text, left + (cellSize - textExtent.x) / 2,
top + (cellSize - textExtent.y) / 2,
SWT.DRAW_TRANSPARENT);
} finally {
gc.setFont(null);
}
}
}
|
O GC utilizado para desenhar o painel é obtido através da implementação de PaintListener em um widget de
tela. A classe SudokuView cria esse controle como um filho direto do shell de nível superior. Ela também
conecta um PaintListener a ele. O painel é então desenhado durante o processamento do evento de pintura da
tela, que fornece acesso a seu contexto gráfico.
A interação do usuário com o jogo é manipulada através de uma combinação de KeyListener e outro comando. O KeyListener é usado para navegação no painel quando o usuário pressiona um botão de navegação direcional (esquerda,
direita, para cima e para baixo). Ele também manipula o teclado numérico — o pressionamento de um número configura a célula
atualmente em destaque para esse valor. Um comando select é utilizado quando o usuário pressiona o botão
Selecionar (no meio do teclado de navegação). Isso aumenta em um o valor da célula atualmente em destaque. A Listagem 6 ilustra
como conectar um listener de evento principal para tratar da entrada do teclado numérico.
Listagem 6. Snippet KeyListener Mostrando como Responder à Navegação com Seta
canvas.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
Cell hoverCell = drawer.getHighlightedCell();
if (hoverCell == null)
return;
if (e.character == SWT.BS || e.character == SWT.DEL) {
hoverCell.clear();
} else if (Character.isDigit(e.character)) {
hoverCell.setValue(e.character - '1' + 1);
} else if (e.keyCode == SWT.SHIFT) {
hoverCell.mark();
} else if (e.keyCode == SWT.ARROW_LEFT) {
Cell cell = hoverCell;
do {
int column = cell.column==0 ? 8 : cell.column-1;
cell = board.getCell(cell.row, column);
} while (cell.isGiven());
drawer.highlightCell(cell);
canvas.redraw();
...
|
|  |