Revista WebMobile 18
Artigo WebMobile 18 - Escrevendo uma aplicação de jogo de cartas
Revista WebMobile Edição 18

Escrevendo uma aplicação de jogo de cartas

Muitos jogos são baseados em baralho de cartas. Portanto faz sentido implementar um conjunto de classes que fornecem um conjunto de métodos para gerenciamento de cartas. Você pode usar estes métodos para implementar vários jogos. Este artigo descreve um mecanismo para distribuir cartas aleatoriamente, exibindo-as na tela, mostrando a “mão” de cada jogador, e gerenciando jogos baseados em cartas. Fornece a implementação no contexto de um jogo de Blackjack funcional, chamado Pocket Jack, para SmartPhone. Para começar, você necessitará do Visual Studio .NET 2003 e do SmartPhone 2003 SDK. Consulte o quadro de links no final do artigo para obter a URL de download do SmartPhone SDK.

Desenhando as cartas na tela

O primeiro problema a resolver é a exibição das cartas de um jogador. As cartas que são usadas no jogo devem ser exibidas na tela do SmartPhone. Para isso, você precisa de 53 imagens: uma para cada carta, mais uma imagem que mostre a parte de trás das cartas. Um conjunto de imagens foi criado em arquivos individuais, como mostrado na Figura 1.

Imagens das cartas
Figura 1. Imagens das cartas.

Cada um desses arquivos é uma imagem GIF da carta em questão. Arquivos numerados de 1 a 52 contêm cada uma das cartas do baralho. O arquivo de número zero é a parte de trás da carta. Estas imagens podem ser adicionadas ao projeto da aplicação e usadas para mostrar as cartas. Cada imagem das cartas possui cerca de 1KB de tamanho, porque contém só quatro cores, então as imagens não usarão muita memória.

Criando o projeto e adicionando imagem de cartas

Para começar, você precisa primeiro criar um novo Smart Device Application no Visual Studio .NET 2003 (Figura 2), para isso você deve seguir os quatro passos descritos a seguir:

  1. No Visual Studio 2003, clique File>New>Project. Isso irá exibir a caixa de diálogo de New Project;
  2. Na caixa de diálogo de New Project, selecione o tipo de projeto Visual C# Projects;
  3. Escolha Smart Device Application;
  4. Atribua o nome de PocketJack para o novo projeto como mostrado na Figura 2.
Caixa de diálogo do New Project
Figura 2. Caixa de diálogo do New Project.

Para completar a criação do projeto de Smart Device Application no Wizard apresentado, selecione a plataforma SmartPhone e o tipo de projeto Windows Application como na Figura 3.

O Smart Device Application Wizard
Figura 3. O Smart Device Application Wizard.

Com o projeto criado, você agora está pronto para incluir as imagens das cartas. O melhor lugar para armazenar as imagens é na própria aplicação – isso torna o deploy mais fácil. É melhor criar um diretório de recursos dentro do projeto que irá armazenar as imagens na pasta principal, porque isso evita o grande número de arquivos e pastas que podem atrapalhar na manutenção do projeto. Para criar a pasta de recursos clique com o botão direito no projeto Add>New Folder, como mostrado na Figura 4.

Adicionando uma nova pasta ao projeto
Figura 4. Adicionando uma nova pasta ao projeto.

Nomeie a pasta para cardImages. Se você olhar o projeto, verá que o Visual Studio criou uma pasta chamada cardImages ligada ao projeto, como mostra a Figura 5.

A pasta cardImages
Figura 5. A pasta cardImages.

Agora que você tem sua pasta no projeto, você precisa colocar as imagens nela. A melhor forma de fazer isso é selecionando todas as imagens no diretório de origem e arrastá-las até a pasta cardImages. Os arquivos aparecem dentro da pasta da Solution, e também são copiados ao diretório do projeto. Entretanto, até o momento, elas não estão embutidas no projeto quando este for distribuído. Para isso acontecer, é preciso marcar os arquivos de imagens como embedded resource, selecionando todas as imagens no diretório. Então, modifique as propriedades Build Action para marcar os arquivos como recursos embutidos (embedded resource), como mostrado na Figura 6.

Recurso de imagens embutidas
Figura 6. Recurso de imagens embutidas.

A aplicação agora tem 52 imagens de cartas e uma imagem de fundo embutidas nela. Se continuar com o projeto verá que o arquivo executável tem em média 83KB de tamanho. A maior parte deste tamanho é das imagens.

Carregando a imagem das cartas

A melhor maneira de gerenciar as cartas é através de um array de cartas. As imagens então poderão ser desenhadas no jogo quando requisitadas. A Listagem 1 nos mostra como armazenar as nossas cartas em um array de imagens.

Listagem 1. Armazenando as cartas em um array de imagens.

//Definindo o tamanho do array das imagens para podermos armazenar as 52 cartas do nosso baralho

System.Drawing.Image[] cardImages = new System.Drawing.Bitmap[53];

//obtemos o Assembly atual para podermos ter acesso ao manifesto de arquivos de recurso

System.Reflection.Assembly execAssem = System.Reflection.Assembly.GetExecutingAssembly();

for (int i = 0; i < 53; i++)

{

    //para cada posição do nosso array criamos um novo bitmap informando o nome completo de cada arquivo

    //lembre-se que foi dado como nome de cada imagem um número e elas estão armazenadas na pasta cardImages do nosso projeto

    cardImages[i] = new System.Drawing.Bitmap(execAssem.GetManifestResourceStream( @"PocketJack.cardImages." + i + @".gif"));

}

Esse código cria um array para guardar referências aos bitmaps e então carrega cada bitmap de seus respectivos recursos dentro do assembly. Perceba que o código cria o nome do recurso usando o número da imagem a ser carregada. Quando criamos recursos embutidos, o Visual Studio automaticamente adiciona o nome do projeto ao começo do nome dos recursos. Além de incluir o nome padrão do projeto, o nome dos recursos também incluem o nome da subpasta, porque os arquivos usados para criá-los estão contidos em uma subpasta do projeto. Como resultado, o código para carregar os recursos deve incluir o nome do projeto padrão e o nome da subpasta como parte do seu nome, para então ser localizado corretamente.

Desenhando a imagem das cartas

O próximo passo a se considerar é o processo de desenhar as cartas. As imagens das cartas têm cantos arredondados. Quando mostradas no plano de fundo do jogo, esses cantos devem ser exibidos de forma transparente, para as cartas parecerem reais. É um pequeno detalhe, mas significante se desejamos que os usuários realmente gostem do jogo. Se olhar atentamente as imagens das cartas, verá que os cantos foram desenhados em verde escuro, como mostra a Figura 7.

Detalhe do canto da carta  8 de espadas
Figura 7. Detalhe do canto da carta 8 de espadas.

Quando as cartas são mostradas, você deve escolher esta cor (o verde) como transparente, para o plano de fundo mostrar a borda de cada carta. Você precisa usar o código da Listagem 2 para criar uma instância de ImageAttributes que indica qual a cor é a transparente.

Listagem 2. Definindo a cor verde como transparente.

//criamos uma instância com os atributos das imagens

System.Drawing.Imaging.ImageAttributes transparentAttributes = new 

System.Drawing.Imaging.ImageAttributes();

//ajustamos a cor verde (Green) para ser identificada como transparente            

transparentAttributes.SetColorKey(Color.Green, Color.Green);

O método SetColorKey informa a faixa de cores a ser considerada como transparente. Para o .NET Compact Framework essas cores devem ter o mesmo valor; isto é, só uma cor pode ser transparente quando mostrada no SmartPhone. Para mostrarmos todas as cartas na nossa aplicação, podemos usar o código da Listagem 3. O programa de exemplo mostra todas as cartas a cada vez que o método Paint é chamado (Figura 8).

Listagem 3. Desenhando as cartas na aplicação de uma forma randômica.

// Cria um objeto para obter o posicionamento randômico das cartas

System.Random rand = new System.Random();

foreach (System.Drawing.Image card in cardImages)

{

    // Cria um retângulo que conterá a imagem a ser desenhada            

    System.Drawing.Rectangle drawRect = new System.Drawing.Rectangle()

    // atribui uma coordenada randômica ao eixo X do retângulo

    drawRect.X = rand.Next(this.Width);

    // atribui uma coordenada randômica ao eixo Y do retângulo

    drawRect.Y = rand.Next(this.Height);

    // atribui o valor correto de largura da imagem

    drawRect.Width = card.Width;

    // atribui o valor correto de altura da imagem

    drawRect.Height = card.Height;

    // cria uma instância de Graphics que será usada para desenhar a carta na tela

    System.Drawing.Graphics GraphicCard = new System.Drawing.Graphics();

    // Desenha a carta na dela conforme os parâmetros informados

    GraphicCard.DrawImage(

        card,                    // Imagem para ser desenhada

        drawRect,                // retângulo randômico que a carta ocupará

        0,                       // posição no eixo X

        0,                       // posição no eixo Y

        card.Width,              // Largura da Imagem

        card.Height,             // Altura da Imagem

        GraphicsUnit.Pixel,      // Unidade de tamanho da imagem

        transparentAttributes);  // Atributos da imagem para tornar a cor verde transparente

}
//www.devmedia.com.br/Imagens/gold/WM/18/artigo6/image8.jpg
Figura 8. Exibição aleatória de cartas na aplicação de exemplo

Classe Card (carta)

Uma instância da classe Card representa cada carta no jogo. Essa classe guarda o valor atual da carta e a mostra na tela. Também fornece propriedades que permitem aos usuários da classe acharem o valor da carta e pegarem o nome da carta, entre outras informações úteis. A classe Card pode ser usada em vários jogos diferentes, mas há algumas características específicas do blackjack.

Acelerando o processo de carregamento

A primeira versão da classe Card fazia a carga de todas as imagens quando a aplicação começa a rodar. Cada uma das 52 imagens das cartas mais a padrão de fundo eram carregadas no começo da aplicação. Isso fez a aplicação começar mais lenta. Uma forma de acelerar esse processo é carregar as imagens sob demanda como mostra o código da Listagem 4.

Listagem 4. Carregando um carta sob demanda.

//Definindo o tamanho do array das imagens para podermos armazenar as 52 cartas do nosso baralho

System.Drawing.Image[] cardImages = new System.Drawing.Bitmap[53];

//se a posição do array for null devemos criar uma nova imagem para esta posição

if (cardImages[dispNo] == null)

{

    //cria e atribui a nova imagem à posição definida por dispNo

    cardImages[dispNo] = new System.Drawing.Bitmap(execAssem.GetManifestResourceStream(

        @"PocketJack.images." + dispNo + @".gif"));

}

A variável cardImages é um array de bitmaps. Todas as imagens de cartas estão vazias. No código anterior, a variável dispNo guarda o número de bitmaps que é preciso. Se o elemento dado for nulo (null), a imagem é carregada, e então pode ser mostrada. A próxima vez que a imagem for exigida, será encontrada imediatamente. Como resultado, a aplicação começa muito mais rápida do que quando todas as imagens eram carregadas, o tempo preciso para carregar somente as cartas exibidas na primeira rodada não é grande. Se suas aplicações necessitam de um grande número de imagens como essa, vale a pena espalhar o tempo de carregamento pela a aplicação em vez de fazê-lo todo no começo.

Jogo de Blackjack

O blackjack (conhecido como 21) é popular faz um bom tempo, é um dos jogos de aposta com cartas mais jogado no mundo. Sacando as cartas e as adicionando a sua mão, o jogador tenta o valor mais próximo do 21 sem ultrapassar este valor, o que é conhecido como “estourar”.

Se o jogador tem um valor de 21 ou menos, ele ou ela deve então ter mais do que o negociante (o banqueiro do jogo, que joga por último) para vencer. Esse artigo descreve cada regra específica porque isso afeta o projeto da aplicação futuramente.

Classe Hand (mão)

Você precisará de uma classe container para guardar as cartas. O jogo terminado precisará de duas instâncias dessa classe: uma para o negociante controlado por computador e uma para o jogador. A classe Hand que você usará guarda um número de cartas. É baseada na coleção ArrayList, que torna mais fácil usuários da classe Hand adicionar e enumerar as cartas em uma mão. Também contém um método que mostrará as cartas em uma mão (DrawHand). O método mostra as cartas começando de uma posição específica na tela. A classe Hand também possui um método que calculará o valor das cartas que tem. Pode ser usado como mostra a Listagem 5.

Listagem 5. Contagem do valor das cartas em uma mão.

public int BlackJackScoreHand()

        {

//Definindo duas variáveis internas para controlar a pontuação e o acesso 

    int score = 0;

    int aces = 0;

    foreach (Card card in this)

    {//para cada carta na instância de Hand acumulamos o valor da propriedade BlackJackScore na nossa variável

 //se o valor acumulado for 11 devemos incrementar a variável aces

    score += card.BlackJackScore;

    if (card.BlackJackScore == 11)

    {

        aces++;

    }

}

//enquanto tivermos um acumulado de pontos maior que 21 (se tivermos estourado) e o nosso aces for maior que 0 iremos diminuir 10 pontos de cada score e um ponto do aces

    while ((score > 21) && (aces > 0))

    {

        score -= 10;

        aces--;

    }

    return score;

}

O método trabalha através de cada uma das cartas da mão. Guarda a contagem do número das cartas que encontrou e então reduz o valor das cartas, onde é apropriado assegurar-se que o valor está mais próximo do 21, possivelmente sem ultrapassá-lo.

Classe CardShoe

A última classe de cartas que é preciso para gerenciar as cartas no jogo é a classe CardShoe. Você usa esta classe para fornecer cartas aleatórias para o jogo. Cassinos têm um dispositivo especial, chamado shoe, que segura as cartas. No começo de um jogo, um número de baralhos são embaralhados juntos e colocados no shoe. O processo para embaralhar que o PocketJack usa trabalha trocando aleatoriamente elementos em posições diferentes em um array de cartas, várias vezes. A classe CardShoe contém esse array e embaralha desta forma no começo do jogo. Aí então abastece cada carta no array até chegar ao fim, no ponto que o array é embaralhado de novo, e o processo se repete.

Projetando para teste

Quando se projeta um sistema, deve-se pensar também em como testá-lo. Seria difícil testar jogando 50 vezes só para ter certeza de que funciona corretamente quando um jogador consegue um valor blackjack de 21. A classe CardShoe foi portanto projetada com uma característica adicional. Além de fornecer um construtor que permite ao desenvolvedor que está usando a classe de selecionar o número de baralhos no “shoe”, fornece um construtor que aceita um array de valores numéricos que representa um baralho empilhado. Tal baralho não está embaralhado, em vez disso irá prover cartas em uma seqüência predeterminada. O baralho empilhado permite ao desenvolvedor testar o comportamento de suas aplicações de jogos de carta fornecendo à aplicação uma sequência definida de cartas.

Para assegurar que o baralho empilhado não seja usado para outros motivos, um flag informa o usuário de uma instância da classe CardShoe se há ou não um baralho empilhado sendo usado no jogo desta forma. Um desenvolver usando a instância da classe pode testar esse flag para ter certeza que a aplicação não está trabalhando com este tipo de baralho empilhado.

Testando a aplicação, pode-se criar comparativamente ocorrências raras como conseguir um blackjack simplesmente fornecendo um baralho empilhado como:


CardShoe shoe = new CardShoe( new byte[] {1,14,11,25} );

O baralho empilhado (código anterior) daria a ambos os jogadores um blackjack. A seqüência de carta 1 representa a primeira carta do primeiro naipe no baralho, que é um Ás.

Por cada naipe conter 13 cartas, 14 representa a primeira carta do segundo naipe, que também é um Ás. Portanto, as pilhas começando com as cartas de seqüência 1 e 14 resultaria em ambos inicialmente recebendo ases. As cartas de sequência 11 e 25 representam o valete do primeiro e segundo naipe respectivamente, então cada um recebe um valete como segunda carta. Isso resulta em ambos conseguindo um blackjack.

Jogando

Quando o jogo começa, a aplicação limpa o que estava em jogo e distribui duas cartas como mostra a Listagem 6. Uma tecla menu chama o método para distribuir outra carta, como mostrado na Listagem 7. Dá ao jogador uma carta adicional desde que o valor em sua mão seja menor que 21.

Listagem 6. Redistribuindo as cartas para cada jogador.

//limpamos os valores da mão atual

playerHand.Clear();

//adicionamos uma carta para a mão

playerHand.Add(shoe.DealCard());

//adicionamos um outra carta para a mão

playerHand.Add(shoe.DealCard());

Listagem 7. Distribuição de uma carta adicional ao jogador desde que a sua pontuação seja menor que 21

//Se o jogador possui menos de 21 na mão

if (playerHand.BlackJackScoreHand() < 21)

    {

//É adicionada uma nova carta à mão do jogador

        playerHand.Add(shoe.DealCard());

//e ao invalidarmos o desenho da classe forçamos a chamada ao método paint

         this.Invalidate();

    }
    

Note que o método BlackJackScoreHand retorna o valor da mão a cada vez. Note também a chamada do método Invalidate. Essa chamada faz o form ser repintado. A aplicação então distribui as cartas e atualiza o valor exibido.

Exibindo os valores do placar

Os métodos para mostrar janelas de textos são bons para mensagens simples, mas para um jogo, um usuário espera algo mais artístico. Por exemplo, você pode mostrar o texto em um plano de fundo com formas para se destacar. Você pode implementar esse plano de fundo mostrando repetidamente o texto em posições em volta do local desejado antes de colocar o verdadeiro texto em cima. Uma maneira de fazer isso foi escrita como parte do conjunto de utilidades, mostrado na Listagem 8.

Listagem 8. Maneira artística de mostrar mensagens ao usuário.

//instância de SolidBrush (tipo de Pincel) para escrevermos as mensagens ao usuário na tela

static private System.Drawing.SolidBrush messageBrush = new System.Drawing.SolidBrush(Color.Black);



public static void BigText(string message, int x, int y, System.Drawing.Color back, System.Drawing.Color fore, System.Drawing.Font messageFont, System.Drawing.Graphics g)

{

    int i;

    //ajustando a cor da mensagem que irá ficar como plano de fundo

    messageBrush.Color = back;

    //um laço de repetição para criar mensagens de fundo em posições aleatórias

    for (i = 1; i < 3; i++)

    {

        g.DrawString(message, messageFont, messageBrush, x - i, y - i);

        g.DrawString(message, messageFont, messageBrush, x - i, y + i);

        g.DrawString(message, messageFont, messageBrush, x + i, y - i);

        g.DrawString(message, messageFont, messageBrush, x + i, y + i);

    }

    //ajusta a cor da mensagem que ficará na frente de todas as mensagens que usamos para dar um ar artístico a nossa mensagem

    messageBrush.Color = fore;

    //escrevemos a mensagem que realmente queremos que o usuário consiga ler, de forma detacada na aplicação

    g.DrawString(message, messageFont, messageBrush, x, y);

}

Esse método é fornecido com uma referência ao objeto Graphics para uso na exibição, da fonte a ser usada, e a posição do texto. Também é dada a cor para as versões do texto no plano de fundo e no primeiro plano. O método é static para ser chamado sem precisar instanciá-lo, é definido como:


public static void BigText(string message, int x, int y, System.Drawing.Color back, System.Drawing.Color fore, System.Drawing.Font messageFont, System.Drawing.Graphics g)” e deve ser usado como “Utilities.BigText("Dealer Bust!",20,80,System.Drawing.Color.Black,System.Drawing.Color.Yellow,messageFont,g);

Método Paint

O método paint é chamado cada vez que a tela precisa ser redesenhada. Do ponto de vista de um projeto, é considerado má prática realizar funções da aplicação dentro deste manipulador, portanto chama um método para isto, que é o método drawHand. Esse método é quem na verdade realiza a exibição das cartas. Ele verifica a mão para se reexibir na tela e depois mostra o placar em uma fonte grande, como especificado na Listagem 9. As cartas são mostradas 20 pixels uma da outra, dando um bom efeito de camada. Então, o método BigText exibe o placar, como mostra a Figura 9.

Listagem 9. Redesenhando a mão e exibindo o score dela na tela.

private void drawHand(Graphics g)

{//Desenhando a mão do jogador(a) na tela

    playerHand.DrawHand(g, 0, 10, 20, 0);

//E informando o score atual do jogador(a)

    Utilities.BigText(

        "Score : " + playerHand.BlackJackScoreHand().ToString(),

        10, 100, System.Drawing.Color.Black, System.Drawing.Color.Yellow, scoreFont, g);

}
Exibindo as cartas e a pontuação
Figura 9. Exibindo as cartas e a pontuação.

A Figura 9 mostra a exibição que o método DrawHand produz. O jogador pode pedir cartas extras até ele ou ela “estourar”. O jogador pode usar a tecla menu para começar um novo jogo ou sair da aplicação.

Jogo de blackjack completo

Você agora tem uma aplicação que implementa a exibição e o gerenciamento de cartas necessários para começar um jogo de blackjack. Na verdade, você pode usar as classes citadas para muitos tipos de jogos de cartas. Agora é preciso considerar como um jogo completo pode ser implementado, permitindo o jogador fazer jogadas e o negociante responder.

Gerenciando estados do jogo

Um jogo de blackjack pode estar em qualquer um dos seguintes estados em algum ponto específico do jogo:

  • O jogador(a) está fazendo suas jogadas;
  • O jogador(a) estourou;
  • O jogador(a) ganhou;
  • O negociante está fazendo suas jogadas;
  • O negociante estourou;
  • O negociante ganhou;
  • Placar empatado.

Em cada um desses estados, a exibição mostrada será diferente, assim como a resposta do jogo a eventos. Por exemplo, a opção de distribuir uma nova carta ao negociante é disponível somente quando for a vez do jogador(a). Você pode representar esses estados na aplicação por meio de tipos enumerados, como exemplificado na Listagem 10.

Listagem 10. Estados possíveis para um jogador.

public enum GameMode

    {

        PlayerActive,

        PlayerWon,

        PlayerBust,

        DealerActive,

        DealerWon,

        DealerBust,

        Push

    }

Uma variável do tipo GameMode conserva o estado do jogo. Essa variável controla o que acontece quando a tela é reexibida. Quando o estado do jogo muda, um conjunto de ações deve ocorrer. A melhor maneira de obter esse comportamento é implementando o gerenciamento de estado por meio de uma propriedade, como mostrado na Listagem 11.

Listagem 11. Controlando o estado da aplicação.

private GameMode modeValue;

//Com a propriedade mode você irá controlar o estado de toda a aplicação

    private GameMode mode

    {

        get

        {//Retorna o estado atual da aplicação

            return modeValue;

        }

        set

        {//Conforme o estado para o qual o jogador irá, a aplicação deve habilitar ou não determinados controles



            switch (value)

            {

                case GameMode.PlayerActive:

                    hitMenuItem.Enabled = true;

                    stayMenuItem.Enabled = true;

                    break;

                case GameMode.PlayerWon:

                case GameMode.PlayerBust:

                case GameMode.DealerActive:

                case GameMode.DealerWon:

                case GameMode.DealerBust:

                case GameMode.Push:

                    hitMenuItem.Enabled = false;

                    stayMenuItem.Enabled = false;

                    break;

            }

//Após definirmos os estados dos controles, guardamos o novo estado e vamos invalidar a pintura da aplicação que irá forçar o processamento do desenho novamente

            modeValue = value;

            this.Invalidate();

        }

    }

Quando a propriedade é associada a um valor, o set da propriedade é executado. Quando o código set é executado, ele realiza uma troca, que deixa a aplicação de forma correta. Por exemplo, quando a propriedade do estado é mudada para o estado PlayerActive, os menus Hit e Stay são ativados. Onde quer que a troca de estado ocorra na aplicação principal, a interface do usuário está sempre no estado correto. Isso também significa que você precisa mudar a configuração do jogo em apenas um lugar do código. Note que quando o estado do jogo muda, o método Invalidate é chamado para assegurar que a tela se mantenha atualizada.

O estado do jogo também controla o que é mostrado quando o método Paint é chamado, como nos mostra a Listagem 12.

Listagem 12. Ajustando o que é mostrado na tela considerando o estado atual do jogo.

private void drawHands(Graphics g)

{

    switch (mode)

    {//Se o jogador for o ativo, redesenhamos a mão do jogador com a nova carta e atualizamos o Score

        case GameMode.PlayerActive:

            dealerHand.DrawHand(g, 10, 5, 80, 0);

            playerHand.DrawHand(g, 10, 110, 20, 0);

            Utilities.BigText(playerHand.BlackJackScoreHand().ToString(),

                140, 150, Color.Black, Color.Yellow, messageFont, g);

            break;

//Se o jogador Ganhou informamos isso ao jogador e redesenhamos a mão vencedora

        case GameMode.PlayerWon:

            dealerHand.DrawHand(g, 10, 5, 20, 0);

            playerHand.DrawHand(g, 10, 110, 20, 0);

            Utilities.BigText(dealerHand.BlackJackScoreHand().ToString(),

            140, 45, Color.Black, Color.Yellow, messageFont, g);

            Utilities.BigText(playerHand.BlackJackScoreHand().ToString(),

                140, 150, Color.Black, Color.Yellow, messageFont, g);

            Utilities.BigText("You Win!",

                20, 80, Color.Black, Color.Yellow, messageFont, g);

            break;

    }

}

O método Paint atualizado mostrado na Listagem 12 mostra como a aplicação gerencia a exibição de mensagens e mãos. Os pontos do negociante não aparecem enquanto o jogador faz suas jogadas.

Preparando o jogo

O negociante deve receber duas cartas, uma virada para baixo. Você consegue isso usando o código da Listagem 13.

Listagem 13. Distribuindo as cartas para o jogador negociante.

// Limpando a mão dos jogadores

playerHand.Clear();

dealerHand.Clear();



// Ajustando a mão do negociante com a carta de face virada para baixo

dealerHoleCard = shoe.DealCard();

dealerHoleCard.FaceUp = false;

dealerHand.Add(dealerHoleCard);



// Ajustando a mão do jogador com uma carta com a face virada para baixo

playerHand.Add(shoe.DealCard());

dealerHand.Add(shoe.DealCard());

playerHand.Add(shoe.DealCard());

// Ajustamos o estado atual do jogo

mode = GameMode.PlayerActive;

A aplicação guarda uma referência para a carta hole (“buraco”) do negociante, que está inicialmente virada para baixo quando uma mão começa. Isto é alcançado colocando a propriedade FaceUp como False. Quando mostrado na tela, a parte de trás da carta buraco será desenhada. Quando o negociante começa a fazer jogadas, a propriedade FaceUp da carta buraco muda para true para que a próxima vez que a mão do negociante for mostrada, a figura da carta estará visível. Perceba a troca de modo no fim do código (PlayerActive), deixando pronto para o jogador participar.

Ações do jogador

As aplicações contém um método que é chamado quando o jogador quer comprar. Ele recebe uma carta adicional (Listagem 14) somente se seu valor for menor que 21.

Listagem 14. Permitindo a compra de uma nova carta.

private void playerHits()

{// Se o jogador tem menos de 21 pontos

    if (playerHand.BlackJackScoreHand() < 21)

    {

// é permitida uma compra de uma nova carta

        playerHand.Add(shoe.DealCard());

// Se após a nova carta o jogador tiver mais de 21 pontos, ele estourou o limite e perde

            if (playerHand.BlackJackScoreHand() > 21)

            {

                mode = GameMode.PlayerBust;

            }

        }

    }

Se o placar do jogador passar de 21, o jogador estoura, e o estado do jogo muda para refletir isto. Do contrário, a exibição é invalidada, o que causa uma nova exibição com a nova carta sendo adicionada na tela. Quando o jogador(a) alcança um valor que o satisfaz, o jogador pode parar selecionando o item apropriado, como mostra o código da Listagem 15.

Listagem 15. O jogador prefere ficar com o valor que o satisfaz.

//se o jogador está com um valor que o satisfaz

private void playerStays()

{

    // a mão do negociante é mostrada

    dealerHoleCard.FaceUp = true;

    mode = GameMode.DealerActive;

    // e iremos ataulizar a interface gráfica

    this.Refresh();

    System.Threading.Thread.Sleep(750);

    //se a mão tiver menos de 17 pontos o jogador negociante recebe cartas até que esse valor ocorra

    while (dealerHand.BlackJackScoreHand() < 17)

    {

        dealerHand.Add(shoe.DealCard());

        this.Refresh();

        System.Threading.Thread.Sleep(750);

    }

    //se o negociante tiver mais que 21 pontos ele perde

    if (dealerHand.BlackJackScoreHand() > 21)

    {

        mode = GameMode.DealerBust;

        return;

    }

    //se o jogador tem mais pontos que o negociante, mas menos de 21 pontos, o jogador ganha

    if (playerHand.BlackJackScoreHand() > dealerHand.BlackJackScoreHand())

    {

        mode = GameMode.PlayerWon;

        return;

    }

    //caso o jogador tenha menos pontos que o negociante, o jogador perde o jogo

    if (playerHand.BlackJackScoreHand() < dealerHand.BlackJackScoreHand())

    {

        mode = GameMode.DealerWon;

        return;

    }

    //se os dois tiverem o mesmo número de pontos o jogo empata

    if (playerHand.BlackJackScoreHand() == dealerHand.BlackJackScoreHand())

    {

        mode = GameMode.Push;

        return;

    }

}

O método tem um pouco de trabalho a se fazer. Ele deve trocar o estado para DealerActive e depois mostrar a mão do negociante. Ele também vira a carta buraco do negociante para o jogador ver. A vez do negociante é jogada como um loop que repetidamente dá novas cartas ao negociante enquanto seu valor for menor que 17. A aplicação contém um atraso de 750 milisegundos entre cada carta do negociante para adicionar “euforia”. Chama o método refresh para assegurar que o jogador é mantido informado sobre cada carta do negociante.

Se o negociante receber uma carta maior que 21, o estado muda para DealerBust, e o método termina. Do contrário, o método decide quem ganhou e coloca o estado que esteja de acordo. O jogador pode então selecionar um novo jogo, que arruma de novo as mãos e o estado apropriado.

Interface de usuário sensível a contexto (context-sensitive)

SmartPhones são restritos pelos controles disponíveis de interface ao usuário. Embora tenha muitas teclas, não tem um mouse ou tela sensível (touch screen), o que significa não poder implementar botões na tela para o usuário selecionar. Além disso, o SmartPhone foi projetado para ser usado com apenas uma mão, o que significa que os controles devem ser fáceis de usar. Em qualquer momento, há um limite de ações que o jogador pode realizar. Para tornar a jogabilidade mais fácil, é sensato mapear essas ações nos controles que o jogador usa mais facilmente (como mostrado no código da Listagem 16). No caso do SmartPhone, isso significa a tecla menu e o joystick.

Listagem 16. Mapeamento de ações aos controles mais usados pelo usuário.

private void doEnter()

{

    switch (mode)

    {//se o modo atual do jogo for a vez do jogador, o tela deve ser atualizada com o score e as cartas do jogador

        case GameMode.PlayerActive:

            playerHits();

            break;

            //caso contrário iniciamos um novo jogo

        case GameMode.PlayerWon:

        case GameMode.PlayerBust:

        case GameMode.DealerActive:

        case GameMode.DealerWon:

        case GameMode.DealerBust:

        case GameMode.Push:

            startGame();

            break;

    }

}

Quando o jogador pressiona o joystick em um SmartPhone, o código anterior decide a coisa mais sensata para a aplicação fazer. No modo PlayerActive, pressionar o joystick distribui outra carta ao jogador. Em qualquer outro modo, distribui as cartas para um novo jogo. Usando também a tecla menu à esquerda para o comando stay (ficar), o usuário pode jogar todo o jogo só usando o joystick e a tecla menu.

Esse assunto deve ser considerado quando se está projetando jogos. Tente jogar com o SmartPhone em uma mão e um xícara de café na outra. Isso deveria ser possível!

Apostas

Em um jogo onde há apenas um jogador é tudo muito bom, mas a maioria dos jogadores quer a chance de “quebrar a banca”. Para fazer isso, o jogo deve programar uma série de mãos, com o jogador apostando uma quantia em cada. A parte de aposta do jogo é realizada antes de cada mão ser jogada. É mostrada ao jogador a aposta disponível e ele pode aumentar ou diminuir antes de jogar. Se os fundos do jogador acabar, a aplicação deve dar a opção de resetar o saldo para o valor original do começo do jogo. Os fundos foram implementados em uma classe separada, para serem usados quando preciso em outros jogos. Antes de cada mão, o jogador decide quanto apostar, até o valor total de seu saldo. Se o jogador tentar apostar mais que no saldo, a aplicação oferece resetar o saldo ao valor inicial.

O jogador gerencia facilmente a aposta movendo o joystick para cima e para baixo. Entretanto, os comandos também estão disponíveis na tecla menu.

Depois de selecionar o tamanho da aposta, o jogador pode começar o jogo apertando a tecla menu ou o joystick. O modo de apostas é na verdade gerenciado por meio de um estado de jogo adicional, que também causa uma imagem de apostas a aparecer, como na Figura 10.

Gerenciando a aposta
Figura 10. Gerenciando a aposta.

Fazendo um jogo completo

A aplicação agora tem todas as características necessárias para jogar Blackjack. Entretanto, algumas adições tornarão o jogo mais conveniente.

Adicionando uma tela de ajuda

A tela de ajuda é na verdade outro form que contém uma única caixa de texto (TextBox) que guarda a informação de ajuda, como mostra a Figura 11. Quando o usuário seleciona a opção de ajuda do menu, o form de ajuda aparece. Quando o form está fechado, a exibição volta para o jogo.

Tela de Ajuda
Figura 11. Tela de Ajuda.

Adicionando som

Um som para um jogo causa boa impressão. Entretanto, por nem todo mundo querer som o tempo todo, você deve fornecer uma opção de desligar.

A classe de utilidades Sound fornece todos os comportamentos de som. Essa classe permite sons serem tocados continuamente como loops ou único som. Para cada som você cria uma instância da classe Sound. Há múltiplos construtores para a classe, (Listagem 17) então você pode criar uma instância usando um caminho para um recurso interno, ou usando um stream.

Listagem 17. Construtores da classe Sound.

private void readStream(System.IO.Stream soundStream)

{

    //Lê a stream

    soundBytes = new byte [soundStream.Length];

    soundStream.Read(soundBytes, 0,(int)soundStream.Length);

}

 

public Sound( System.IO.Stream soundStream)   

{

    //Chama readStream

    readStream(soundStream);

}

 

public Sound ( string filename )

{

// Via reflection vamos buscar o assembly atual

    System.Reflection.Assembly execAssem =

        System.Reflection.Assembly.GetExecutingAssembly();

// Buscamos o caminho do arquivo do manifesto de recursos do nosso assembly

    System.IO.Stream soundStream = execAssem.GetManifestResourceStream(filename);

    readStream ( soundStream );

}

O método readStream realiza o carregamento do som atual. Um método nativo do sistema operacional (Listagem 19) controla a geração do som. A classe Sound contém um platform invoke (P/Invoke) ligado ao método para que possa ser chamado para controlar o som.

Listagem 18. Enum da geração do som.

private enum Flags

{

    SND_SYNC = 0x0000,        

    SND_ASYNC = 0x0001,     

    SND_NODEFAULT = 0x0002, 

    SND_MEMORY = 0x0004, 

    SND_LOOP = 0x0008, 

    SND_NOSTOP = 0x0010, 

    SND_NOWAIT = 0x00002000,

    SND_ALIAS = 0x00010000,

    SND_ALIAS_ID = 0x00110000,

    SND_FILENAME = 0x00020000,

    SND_RESOURCE = 0x00040004 

}

 Listagem 19. DLLImport para PlaySound

[DllImport("CoreDll.DLL", EntryPoint = "PlaySound", SetLastError = True)]

private extern static int  WCE_PlaySoundBytes(Byte[] szSound, IntPtr hMod, int flags);

Um conjunto de flags (Listagem 18) controla a própria geração do som. A classe Sound contém um membro estático que guarda a referência ao som de loop atual (se houver). Se o som for desligado e ligado de novo, o som pode continuar a tocar. A instância de Sound contém os métodos Play e PlayLoop para selecionar um tipo de toque específico (Listagem 20).

Listagem 20. Método para tocar o som uma vez ou em loop.


    public void Play()

    {

        loopSound = null;

        if (Sound.Enabled)

        {

            WCE_PlaySoundBytes(

                soundBytes,

                IntPtr.Zero,

                 (int)(Flags.SND_ASYNC | Flags.SND_MEMORY)

                );

        }

    }

    public void PlayLoop()

    {

        loopSound = soundBytes;

        if (Sound.Enabled)

        {

            WCE_PlaySoundBytes(

                soundBytes,

                IntPtr.Zero,

                 (int)(Flags.SND_ASYNC | Flags.SND_MEMORY | Flags.SND_LOOP)

                );

        }

    }

A classe Sound deve parar de tocar sons quando o usuário sai da tela de jogo. Quando o jogador retornar à tela do jogo, os sons devem continuar. Os eventos que controlam esse comportamento são os Deactivate e Activate. Esses eventos não estão disponíveis no editor de eventos para a tela, então você precisa colocá-los no método construtor (Listagem 21).

Listagem 21. Manipulação dos eventos Activated e Deactivate.

this.Activated += new EventHandler(this.OnActivate);

this.Deactivate += new EventHandler(this.OnDeActivate);

 

    void OnActivate(object sender, EventArgs e)

    {

        Sound.ResumeSound();

    }



    void OnDeActivate(object sender, EventArgs e)

    {

        Sound.StopSound();

    }

Os métodos OnActivate e OnDeActivate são chamados quando a tela é ativada ou desativada, respectivamente. Eles chamam métodos estáticos na classe Sound para controlar e tocar o som.

O método StopSound (Listagem 22) na classe Sound pára de tocar o som presente requisitando um som null imediatamente. Já o método ResumeSound (Listagem 23) só retorna um som se um som em loop estava tocando anteriormente.

Listagem 22. Método responsável por finalizar a execução do som.

public static void StopSound()

    {

        if (loopSound != null)

        {

            WCE_PlaySoundBytes(

                null,

                IntPtr.Zero,

                0

                );

        }

    }
Listagem 23. Método responsável por retornar o som.

    public static void ResumeSound()

    {

        if (!Sound.Enabled)

        {

            return;

        }

        if (loopSound != null)

        {

            WCE_PlaySoundBytes(

                loopSound,

                IntPtr.Zero,

                 (int)(Flags.SND_ASYNC | Flags.SND_MEMORY | Flags.SND_LOOP)

                );

        }

    }

 

O membro enabled da classe é a flag que indica se o som está habilitado. Se falso, o som não retorna ou toca. A seleção de uma opção do menu gerencia este flag, como mostra a Figura 12.

Opção de som no menu
Figura 12. Opção de som no menu.

Conclusão

Esse artigo descreve a implementação de um jogo de Blackjack completo que você pode usar por horas. Ele deve ser muito útil como repositório de métodos e técnicas que você pode desenvolver quando escrever seu próprio jogo de cartas para SmartPhone.