Jogo da velha (C++)
Eu estava com vontade de praticar um pouco a linguagem de programação C++, pois, tenho pouca experiência na linguagem. Então, decidi implementar um Jogo da Velha em C++. E assim fiz. Projetei o sistema para ser flexível a mudanças, o que facilita a possibilidade de um trabalho em equipe. No entanto, foi mais viável implementar o Jogo da Velha em C (Uma linguagem de programação estruturada), por tratar-se de um sistema relativamente simples e por ter sido feito apenas por uma pessoa: Eu. Inclusive, caso queira, você pode visitar a página onde escrevi sobre o desenvolvimento de um Jogo da Velha em C clicando aqui, onde, disponibilizei, também, versões em Java e Android.
Pular para a sessão de downloads
O código fonte está distribuído entre os pacotes: modelo, gui, controlador, grafico. No pacote modelo ficam as classes que representam a lógica do sistema. Abaixo, o diagrama de classes que representa o módulo "modelo":
De acordo com o modelo acima, os métodos das coordenadas e dimensões dos objetos desenhados pelo jogo, ficam na classe Tabuleiro. Na classe Placar, ficam armazenados os valores das quantidades de vitórias, empates e derrotas. Na classe Jogo ficam as funções da lógica do sistema, inclusive, nesta classe, há uma implementação de um algoritmo de inteligência artificial: O MINIMAX
.No pacote “gui” ficam as classes de interface gráfica com o usuário, onde, foi implementado o tratamento de eventos do mouse, teclas e janela. Abaixo o diagrama de classes que representa o módulo "gui":
No pacote "gui" existe uma classe chamada GUI onde fica o loop principal do jogo. Nesse loop, são tratados os eventos e os devidos métodos implementados na classe GUI_Controlador são chamados. Para que isso funcione, é claro, é necessário que a classe GUI_Controlador implemente a interface GUI_Listener e seus métodos e seja registrada na classe GUI, através do método setGUIListener. Por exemplo, veja o código abaixo:
GUI_Controlador* guiCtrl = new GUI_Controlador( jogo, gui );
gui->setGUIListener( guiCtrl );
Veja o loop principal na classe GUI:
fim = false;
while( !fim ) {
grafico->desenha( renderizador );
if ( listener != NULL )
listener->desenhou();
while( SDL_PollEvent( &evento ) != 0 ) {
if ( listener != NULL ) {
switch( evento.type ) {
case SDL_QUIT:
listener->janela_fechada();
fim = true;
break;
case SDL_MOUSEBUTTONDOWN:
listener->mouse_pressionado( evento.motion.x, evento.motion.y );
break;
case SDL_KEYDOWN:
listener->tecla_pressionada( evento.key.keysym.sym );
break;
}
}
}
SDL_Delay( Consts::DELAY );
}
No exemplo acima, repare nos métodos do objeto listener. São eles: desenhou, janela_fechada, mouse_pressionado, tecla_pressionada. A classe GUI_Controlador implementa esses métodos, porque, estão definidos na interface GUI_Listener (Arquivo cabeçalho .h). Esta é uma implementação do padrão de projetos Observer aplicado para desacoplar a lógica do jogo do código de interface gráfica. Repare também que existe um objeto de nome grafico, onde o método desenha é chamado. Assim como o registro de uma implementação da interface GUI_Listener, existe também um registro como a seguir:
Grafico* grafico = new JogoGrafico( jogo, gui );
gui->setGrafico( grafico );
Veja abaixo o diagrama que ilustra a relação de implementação entre GUI_Listener e GUI_Controller:
Na classe GUI_Controlador:
GUI_Controlador::GUI_Controlador( Jogo* jogo, GUI* gui ) {
this->jogo = jogo;
this->gui = gui;
}
void GUI_Controlador::mouse_pressionado( int x, int y ) {
... // implementação omitida para simplificar o exemplo
}
void GUI_Controlador::tecla_pressionada( char key ) {
... // implementação omitida para simplificar o exemplo
}
void GUI_Controlador::desenhou() {
... // implementação omitida para simplificar o exemplo
}
void GUI_Controlador::janela_fechada() {
}
Repare nos métodos implementados: mouse_pressionado, tecla_pressionada, desenhou, janela_fechada. São todos chamados no loop principal presente na classe GUI.
Outra estratégia parecida (mas não igual) é aplicada para separar a implementação dos gráficos da interface gráfica com usuário. Na classe GUI existe, além do método setGUIListener, o método setGrafico. Onde, se pode passar como parâmetro uma implementação da interface (grafico.h) conforme foi explicado mais atrás. Abaixo o diagrama de classes que ilustra o relacionamento entre a classe JogoGrafico que implementa a interface grafico (grafico.h) e outras classes de desenho na tela do jogo:
O MINIMAX
O algoritmo de inteligência artificial simbólica MINIMAX foi implementado para a inteligência do jogador computador. Conforme a fonte WIKIPEDIA (pt.wikipedia.org), o algoritmo MINIMAX pode ser representado em pseudocódigo conforme a seguir:
ROTINA minimax(nó, profundidade, maximizador)
SE nó é um nó terminal OU profundidade = 0 ENTÃO
RETORNE o valor da heurística do nó
SENÃO SE maximizador é FALSE ENTÃO
α ← +∞
PARA CADA filho DE nó
α ← min(α, minimax(filho, profundidade-1,true))
FIM PARA
RETORNE α
SENÃO
α ← -∞
PARA CADA filho DE nó
α ← max(α, minimax(filho, profundidade-1,false))
FIM PARA
RETORNE α
FIM SE
FIM ROTINA
Veja abaixo como eu implantei o algoritmo MINIMAX:
int Jogo::_minimax( char js[3][3], int nivel, bool maximizador ) {
if ( verificaSeVenceu( js, O ) )
return 1;
if ( verificaSeVenceu( js, X ) )
return -1;
if ( verificaSeEmpate( js ) )
return 0;
if ( nivel <= 0 )
return 0;
if ( maximizador ) {
int min = INT_MAX;
for( int i = 0; i <= 2; i++ ) {
for( int j = 0; j <= 2; j++ ) {
if ( js[i][j] == V ) {
char aux[3][3];
copiaJogadas( aux, js );
aux[i][j] = X;
int p = _minimax( aux, nivel-1, false );
if ( p <= min )
min = p;
}
}
}
return min;
} else {
int max = INT_MIN;
for( int i = 0; i <= 2; i++ ) {
for( int j = 0; j <= 2; j++ ) {
if ( js[i][j] == V ) {
char aux[3][3];
copiaJogadas( aux, js );
aux[i][j] = O;
int p = _minimax( aux, nivel-1, true );
if ( p >= max )
max = p;
}
}
}
return max;
}
}
void Jogo::minimax( int *posX, int *posY ) {
int pontos = INT_MIN;
int vet[9][3];
int cont = 0;
for( int i = 0; i <= 2; i++ ) {
for( int j = 0; j <= 2; j++ ) {
if ( jogadas[i][j] == V ) {
char aux[3][3];
copiaJogadas( aux, jogadas );
aux[i][j] = O;
if ( verificaSeVenceu( aux, O ) || verificaSeEmpate( aux ) ) {
*posX = i;
*posY = j;
return;
}
int p = _minimax( aux, 9-contaJogadas( aux ), true );
if ( p >= pontos ) {
vet[cont][0] = i;
vet[cont][1] = j;
vet[cont][2] = p;
cont++;
pontos = p;
}
}
}
}
int vet2[9][2];
int cont2 = 0;
for( int k = 0; k < cont; k++ ) {
if ( vet[k][2] == pontos ) {
vet2[cont2][0] = vet[k][0];
vet2[cont2][1] = vet[k][1];
cont2++;
}
}
int i = rand() % cont2;
*posX = vet2[ i ][ 0 ];
*posY = vet2[ i ][ 1 ];
}
Onde, js é uma matriz de jogadas que representa uma possibilidade no tabuleiro. Ex: com três jogadas no tabuleiro, uma possibilidade é a celula (2, 2) igual a "X", a célula (1, 1) igual a "O" a célula (1,2) igual a "X" e o restante, igual a VAZIO. Inicialmente, a matriz js é uma cópia da matriz jogadas da classe Jogo.
Exemplo de matriz js:
char js[3][3] = {
{ ' ', ' ', ' '},
{ ' ', 'O', ' '},
{ ' ', 'X', 'X'}
};
Concluindo...
O desenvolvimento do Jogo da Velha em C++ foi para mim uma ótima oportunidade de aprender uma nova linguagem de programação. Tive a oportunidade de aplicar padrões de projeto para separar (desacoplar) os módulos do sistema de modo a tornar possível o trabalho em equipe. Embora, eu tenha construido o sistema sozinho. Inclusive, gostaria de ter a oportunidade de desenvolver algum software em equipe para por em prática o conhecimento e experiência que desenvolvi até agora na área de analise de sistemas.
Downloads
Programa executável: jogodavelha.rar.
Projeto no dev-c++: jogodavelha-src.rar.
Caso alguem queira compilar o código fonte, entre em contato comigo (Pode deixar um comentário). Que eu oriento sobre como fazer. Posso adiantar que o sistema depende da biblioteca SDL para funcionar. Por isso, é necessário configurar a biblioteca no ambiente de desenvolvimento escolhido.
Até o próximo pessoal!