Exercício – Lógica do jogo

Concluído

Neste exercício, adicionamos lógica de jogo ao nosso aplicativo para garantir que acabemos com um jogo totalmente funcional.

Para ajudar a manter este tutorial no tópico ensinando sobre Blazor, fornecemos uma classe chamada GameState que contém a lógica para gerenciar o jogo.

Adicionar o estado do jogo

Vamos adicionar a classe GameState ao projeto e disponibilizá-la aos componentes como um serviço singleton por meio da injeção de dependência.

  1. Copie o arquivo GameState.cs na raiz do projeto.

  2. Abra o arquivo Program.cs na raiz do projeto e adicione essa instrução para configurar GameState como um serviço singleton no seu aplicativo:

    builder.Services.AddSingleton<GameState>();
    

    Agora podemos injetar uma instância da classe GameState em nosso componente Board.

  3. Adicione a seguinte diretiva @inject no início do arquivo Board.razor. A diretiva injeta o estado atual do jogo no componente:

    @inject GameState State
    

    Agora podemos começar a conectar nosso componente Board ao estado do jogo.

Redefinir estado

Vamos começar redefinindo o estado do jogo quando o componente Board for pintado na tela pela primeira vez. Adicione um trecho de código para redefinir o estado do jogo quando o componente é inicializado.

  1. Adicione um método OnInitialized com uma chamada para ResetBoard, dentro do bloco @code na parte inferior do arquivo Board.razor da seguinte maneira:

    @code {
        protected override void OnInitialized()
        {
            State.ResetBoard();
        }
    }
    

    Quando o tabuleiro é mostrado pela primeira vez a um usuário, o estado é redefinido para o início de um jogo.

Criar peças de jogo

Em seguida, vamos alocar as possíveis 42 peças de jogo que poderiam ser jogadas. Podemos representar as partes do jogo como uma matriz referenciada por 42 elementos HTML no tabuleiro. Podemos mover e colocar essas peças atribuindo um conjunto de classes CSS com posições de coluna e linha.

  1. Para manter nossas peças do jogo, definimos um campo de matriz de cadeia de caracteres no bloco de códigos:

    private string[] pieces = new string[42];
    
  2. Adicione código à seção HTML que cria 42 tags span, uma para cada peça do jogo, no mesmo componente:

    @for (var i = 0; i < 42; i++)
    {
       <span class="@pieces[i]"></span>
    }
    

    Seu código completo deve ter esta aparência:

    <div>
        <div class="board">
        @for (var i = 0; i < 42; i++)
        {
            <span class="container">
                <span></span>
            </span>
        }
        </div>
        @for (var i = 0; i < 42; i++)
        {
           <span class="@pieces[i]"></span>
        }
    </div>
    @code {
        private string[] pieces = new string[42];
    
        protected override void OnInitialized()
        {
            State.ResetBoard();
        }
    }
    

    Isso atribui uma cadeia de caracteres vazia à classe CSS de cada intervalo de peças do jogo. Uma cadeia de caracteres vazia para uma classe CSS impede que as peças do jogo apareçam na tela, pois nenhum estilo é aplicado a elas.

Manipular o posicionamento de peças do jogo

Vamos adicionar um método para identificar quando um jogador colocar uma peça em uma coluna. A classe GameState sabe como atribuir a linha correta para a peça do jogo e relata a linha em que ela chega. Podemos usar essas informações para atribuir classes CSS que representam a cor do jogador, a localização final da peça e uma animação de queda CSS.

Chamamos esse método PlayPiece; ele aceita um parâmetro de entrada de dados que especifica a coluna escolhida pelo jogador.

  1. Adicione este código abaixo da matriz pieces que definimos na etapa anterior.

    private void PlayPiece(byte col)
    {
        var player = State.PlayerTurn;
        var turn = State.CurrentTurn;
        var landingRow = State.PlayPiece(col);
        pieces[turn] = $"player{player} col{col} drop{landingRow}";
    }
    

Confira o que o código PlayPiece faz:

  1. Dizemos ao estado do jogo para reproduzir uma peça na coluna enviada chamada col e capturar a linha em que a peça caiu.
  2. Em seguida, podemos definir as três classes CSS a serem atribuídas à peça do jogo para identificar qual jogador está atuando no momento, a coluna em que a peça foi colocada e a linha de aterrissagem.
  3. A última linha do método atribui essas classes a essa peça de jogo na matriz pieces.

Se procurar no Board.razor.css fornecido, você encontrará as classes de CSS que correspondem a coluna, linha e vez do jogador.

O efeito resultante é que a peça do jogo é colocada na coluna e animada para cair na última linha quando esse método é chamado.

Escolher uma cor

Em seguida, precisamos colocar alguns controles que permitem que os jogadores escolham uma coluna e chamem nosso novo método PlayPiece. Usamos o caractere "🔽" para indicar que você pode soltar uma peça nessa coluna.

  1. Acima da marca inicial <div>, adicione uma linha de botões clicáveis:

    <nav>
        @for (byte i = 0; i < 7; i++)
        {
            var col = i;
            <span title="Click to play a piece" @onclick="() => PlayPiece(col)">🔽</span>
        }
    </nav>
    

    O atributo @onclick especifica um manipulador de eventos para o evento de clique. Mas para manipular eventos de interface do usuário, um componente Blazor precisa ser renderizado usando um modo de renderização interativo. Por padrão, os componentes Blazor são renderizados estaticamente do servidor. Podemos aplicar um modo de renderização interativo a um componente usando o atributo @rendermode.

  2. Atualize o componente Board na página Home para que ele use o modo de renderização InteractiveServer.

    <Board @rendermode="InteractiveServer" />
    

    O modo de renderização InteractiveServer lida com os eventos de interface do usuário de seus componentes a partir do servidor por meio de uma conexão WebSocket com o navegador.

  3. Execute o aplicativo com essas alterações. Agora, isso deverá ser parecido com:

    Captura de tela do Connect Four Board.

    Melhor ainda, quando selecionamos um dos botões de soltar na parte superior, o seguinte comportamento pode ser observado:

    Captura de tela da animação do Connect Four.

Esse é um grande progresso! Agora podemos adicionar peças ao tabuleiro. O objeto GameState é inteligente o suficiente para alternar entre os dois jogadores. Vá em frente e selecione mais botões de soltar e observe os resultados.

Vitória e tratamento de erros

Se jogar o jogo na configuração atual, você vai descobrir que ele gera erros quando você tenta colocar muitas peças na mesma coluna e quando um jogador ganha o jogo.

Vamos deixar claro o estado atual do nosso jogo adicionando alguns indicadores e tratamentos de erros ao nosso tabuleiro. Adicione uma área de status acima do tabuleiro e abaixo dos botões de soltar.

  1. Insira a seguinte marcação após o elemento nav:

    <article>
        @winnerMessage  <button style="@ResetStyle" @onclick="ResetGame">Reset the game</button>
        <br />
        <span class="alert-danger">@errorMessage</span>
        <span class="alert-info">@CurrentTurn</span>
    </article>
    

    Essa marcação nos permite exibir indicadores para:

    • Anunciar um vencedor do jogo
    • Um botão que nos permite reiniciar o jogo
    • Mensagens de erro
    • A vez do jogador atual

    Agora vamos preencher um trecho de lógica que defina esses valores.

  2. Adicione o seguinte código após a matriz de peças:

    private string[] pieces = new string[42];
    private string winnerMessage = string.Empty;
    private string errorMessage = string.Empty;
    
    private string CurrentTurn => (winnerMessage == string.Empty) ? $"Player {State.PlayerTurn}'s Turn" : "";
    private string ResetStyle => (winnerMessage == string.Empty) ? "display: none;" : "";
    
    • A propriedade CurrentTurn é calculada automaticamente com base no estado do winnerMessage e na propriedade PlayerTurn do GameState.
    • O ResetStyle é calculado com base no conteúdo do WinnerMessage. Se houver um winnerMessage, faremos com que o botão redefinir apareça na tela.
  3. Vamos lidar com a mensagem de erro quando uma peça é reproduzida. Adicione uma linha para limpar a mensagem de erro e, em seguida, encapsule o código no método PlayPiece com um bloco try...catch para definir o errorMessage, caso tenha ocorrido uma exceção:

    errorMessage = string.Empty;
    try
    {
        var player = State.PlayerTurn;
        var turn = State.CurrentTurn;
        var landingRow = State.PlayPiece(col);
        pieces[turn] = $"player{player} col{col} drop{landingRow}";
    }
    catch (ArgumentException ex)
    {
        errorMessage = ex.Message;
    }
    

    Nosso indicador de manipulador de erros é simples e usa a estrutura do CSS de Inicialização para exibir um erro no modo de perigo.

    Captura de tela do seu jogo até agora, com um tabuleiro e peças.

  4. Em seguida, vamos adicionar o método ResetGame que nosso botão dispara para reiniciar um jogo. Atualmente, a única maneira de reiniciar um jogo é atualizando a página. Esse código permite permanecer na mesma página.

    void ResetGame()
    {
        State.ResetBoard();
        winnerMessage = string.Empty;
        errorMessage = string.Empty;
        pieces = new string[42];
    }
    

    Agora, nosso método ResetGame tem a seguinte lógica:

    • Redefinir o estado do quadro.
    • Ocultar nossos indicadores.
    • Redefinir a matriz peças para uma matriz vazia de 42 cadeias de caracteres.

    Essa atualização deve nos permitir jogar o jogo novamente, e agora vemos um indicador logo acima do tabuleiro declarando a vez do jogador e, eventualmente, a conclusão do jogo.

    Captura de tela que exibe o fim do jogo.

    Ainda temos uma situação em que não podemos selecionar o botão de redefinição. Vamos adicionar um trecho de lógica ao método PlayPiece para detectar o final do jogo.

  5. Vamos detectar se há um vencedor no jogo adicionando uma expressão de comutador após nosso bloco try...catch em PlayPiece.

    winnerMessage = State.CheckForWin() switch
    {
        GameState.WinState.Player1_Wins => "Player 1 Wins!",
        GameState.WinState.Player2_Wins => "Player 2 Wins!",
        GameState.WinState.Tie => "It's a tie!",
        _ => ""
    };
    

    O método CheckForWin retorna uma enumeração que relata qual jogador ganhou, se algum tiver ganhado, ou se o resultado do jogo é um empate. Essa expressão de switch definirá o campo winnerMessage adequadamente se ocorrer um estado de fim de jogo.

    Agora, quando jogamos e chegamos a um cenário de fim de jogo, esses indicadores aparecem:

    Captura de tela que mostra como redefinir o jogo.

Resumo

Aprendemos muito sobre o Blazor e desenvolvemos um joguinho bem bacana. Aqui estão algumas das habilidades que aprendemos:

  • Criar um componente
  • Adicionar esse componente à nossa home page
  • Injeção de dependência usada para gerenciar o estado de um jogo
  • Tornar o jogo interativo com manipuladores de eventos para colocar peças e redefinir o jogo
  • Escrever um manipulador de erros para relatar o estado do jogo
  • Parâmetros adicionados ao nosso componente

O projeto que criamos é um jogo simples, e há muito mais que você poderia fazer com ele. Procurando alguns desafios para melhorá-lo?

Desafios

Considere o seguinte trecho de C#:

  • Para diminuir o aplicativo, remova o layout padrão e as páginas adicionais.
  • Aprimore os parâmetros do componente Board para que você possa passar todo valor de cor CSS válido.
  • Aprimore a aparência dos indicadores com alguns layouts CSS e HTML.
  • Introduza efeitos sonoros.
  • Adicione um indicador visual e impeça que um botão de soltar seja usado quando a coluna estiver cheia.
  • Adicione recursos de rede para que você possa jogar com um amigo no navegador dele.
  • Insira o jogo em um .NET MAUI com aplicativo Blazor e reproduza-o em seu telefone ou tablet.

Feliz programação e divirta-se!