Esercizio - Logica del gioco

Completato

In questo esercizio si aggiunge la logica del gioco all'app per assicurarsi che il gioco sia completamente funzionante.

Per mantenere questa esercitazione in argomento con la formazione su Blazor, viene fornita una classe denominata GameState che contiene la logica per la gestione del gioco.

Aggiunta dello stato del gioco

Aggiungere la classe GameState al progetto, quindi renderla disponibile ai componenti come servizio singleton tramite inserimento delle dipendenze.

  1. Copiare il file GameState.cs nella radice del progetto.

  2. Aprire il file Program.cs nella radice del progetto e aggiungere questa istruzione che configura GameState come servizio singleton nell'app:

    builder.Services.AddSingleton<GameState>();
    

    È ora possibile inserire un'istanza della classe GameState nel componente Board.

  3. Aggiungere la direttiva @inject seguente all'inizio del file Board.razor. la direttiva inserisce lo stato corrente del gioco nel componente:

    @inject GameState State
    

    È ora possibile iniziare a connettere il componente Board allo stato del gioco.

Reimpostare lo stato

Per iniziare, verrà reimpostato lo stato del gioco quando il componente Board viene disegnato per la prima volta sullo schermo. Aggiungere il codice per reimpostare lo stato del gioco quando il componente viene inizializzato.

  1. Aggiungere un metodo OnInitialized con una chiamata a ResetBoard, all'interno del blocco @code nella parte inferiore del file Board.razor come indicato di seguito:

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

    Quando il tabellone viene mostrato per la prima volta a un utente, lo stato viene reimpostato all'inizio di un gioco.

Creare le pedine del gioco

Successivamente, verranno allocate le 42 pedine del gioco che è possibile giocare. È possibile rappresentare le pedine del gioco come matrice a cui fanno riferimento 42 elementi HTML sul tabellone. È possibile spostare e posizionare tali pedine assegnando un set di classi CSS con posizioni di colonna e di riga.

  1. Per contenere le pedine del gioco, definiamo un campo di matrice stringhe nel blocco di codice:

    private string[] pieces = new string[42];
    
  2. Aggiungere il codice alla sezione HTML che crea 42 tag span, uno per ogni pedina del gioco, nello stesso componente:

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

    Il codice completo sarà simile al seguente:

    <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();
        }
    }
    

    In questo modo viene assegnata una stringa vuota alla classe CSS di ogni intervallo delle pedine del gioco. Una stringa vuota per una classe CSS impedisce la visualizzazione delle pedine del gioco sullo schermo in quanto a esse non è stato applicato alcuno stile.

Gestire il posizionamento delle pedine del gioco

Verrà aggiunto un metodo per gestire quando un giocatore inserisce una pedina in una colonna. La classe GameState sa come assegnare la riga corretta per la pedina del gioco e segnala la riga in cui atterra. È possibile usare queste informazioni per assegnare classi CSS che rappresentano il colore del giocatore, la posizione finale della pedina e un'animazione di caduta CSS.

Viene chiamato il metodo PlayPiece, che accetta un parametro di input che specifica la colonna scelta dal giocatore.

  1. Aggiungere questo codice sotto la matrice pieces definita nel passaggio precedente.

    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}";
    }
    

Ecco cosa fa il codice PlayPiece:

  1. Si indica allo stato del gioco di giocare una pedina nella colonna specificata chiamata col e si acquisisce la riga in cui è atterrato il pezzo.
  2. È quindi possibile definire le tre classi CSS da assegnare alla pedina deli gioco per identificare il giocatore attualmente in azione, la colonna in cui è stata inserita la pedina e la riga di destinazione.
  3. L'ultima riga del metodo assegna queste classi alla pedina del gioco nella matrice pieces.

Se si esamina l'elemento Board.razor.css fornito, si trovano le classi CSS corrispondenti per colonna, riga e turno del giocatore.

L'effetto risultante è che la pedina del gioco viene inserita nella colonna e animata per cadere nella riga più in basso quando viene chiamato questo metodo.

Scegliere una colonna

Successivamente, è necessario posizionare alcuni controlli che consentono ai giocatori di scegliere una colonna e chiamare il nuovo metodo PlayPiece. Viene usato il carattere "🔽" per indicare che è possibile far cadere una pedina in questa colonna.

  1. Sopra il tag <div> iniziale, aggiungere una riga di pulsanti:

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

    L'attributo @onclick specifica un gestore eventi per l'evento click. Tuttavia, per gestire gli eventi dell'interfaccia utente, è necessario eseguire il rendering di un componente Blazor usando una modalità di rendering interattiva. Per impostazione predefinita, il rendering dei componenti Blazor viene eseguito in modo statico dal server. È possibile applicare una modalità di rendering interattiva a un componente usando l'attributo @rendermode.

  2. Aggiornare il componente Board nella pagina Home in modo che usi la modalità di rendering InteractiveServer.

    <Board @rendermode="InteractiveServer" />
    

    La modalità di rendering InteractiveServer gestisce gli eventi dell'interfaccia utente per i componenti del server tramite una connessione WebSocket con il browser.

  3. Eseguire l'app con queste modifiche. Ora dovrebbe essere simile a quanto segue:

    Screenshot del tabellone del gioco Forza quattro.

    Ancora meglio, quando si seleziona uno dei pulsanti di rilascio nella parte superiore, è possibile osservare il comportamento seguente:

    Screenshot dell'animazione di Forza quattro.

È un grande passo avanti. A questo punto, è possibile aggiungere le pedine al tabellone. L'oggetto GameState è abbastanza intelligente da alternarsi tra i due giocatori. Procedere e selezionare altri pulsanti di rilascio e osservare i risultati.

Vincita e gestione degli errori

Se si gioca nella configurazione corrente, si scopre che il gioco genera errori quando si tenta di inserire troppe pedine nella stessa colonna e quando un giocatore vince la partita.

Per chiarire lo stato attuale del gioco, aggiungere alcuni indicatori e la gestione degli errori al tabellone. Aggiungere un'area di notifica sopra il tabellone e sotto i pulsanti di rilascio.

  1. Inserire il markup seguente dopo l'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>
    

    Questo markup consente di visualizzare gli indicatori per:

    • Annunciare il vincitore del gioco
    • Un pulsante che ci consente di riavviare il gioco
    • Messaggi di errore
    • Turno del giocatore corrente

    A questo punto, compilare una logica che imposta questi valori.

  2. Aggiungere il codice seguente dopo la matrice di pedine:

    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;" : "";
    
    • La proprietà CurrentTurn viene calcolata automaticamente in base allo stato dell'oggetto winnerMessage e alla proprietà PlayerTurn dell'oggetto GameState.
    • L'oggetto ResetStyle viene calcolato in base al contenuto dell'oggetto WinnerMessage. Se è presente un messaggio winnerMessage, il pulsante di reimpostazione viene visualizzato sullo schermo.
  3. Ora si gestirà il messaggio di errore quando viene giocata una pedina. Aggiungere una riga per cancellare il messaggio di errore e quindi eseguire il wrapping del codice nel metodo PlayPiece con un blocco try...catch per impostare il messaggio errorMessage se si è verificata un'eccezione:

    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;
    }
    

    L'indicatore del gestore degli errori è semplice e usa il framework CSS Bootstrap per visualizzare un errore in modalità pericolo.

    Screenshot del gioco a questo punto, con un tabellone e le pedine.

  4. Aggiungere quindi il metodo ResetGame attivato dal pulsante per riavviare il gioco. Attualmente, l'unico modo per riavviare una partita consiste nell'aggiornare la pagina. Questo codice consente di rimanere nella stessa pagina.

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

    A questo punto, il metodo ResetGame ha la logica seguente:

    • Reimpostare lo stato del tabellone.
    • Nascondere gli indicatori.
    • Reimpostare la matrice di pedine su una matrice vuota di 42 stringhe.

    Questo aggiornamento dovrebbe consentire di giocare di nuovo e verrà visualizzato un indicatore appena sopra tabellone che dichiara il turno del giocatore e alla fine il completamento del gioco.

    Screenshot che visualizza la fine del gioco.

    Si ha una situazione in cui tuttavia non è possibile selezionare il pulsante reset. A tale scopo, si aggiungerà al metodo PlayPiece la logica che rileva la fine della partita.

  5. È possibile rilevare se è presente un vincitore del gioco aggiungendo un'espressione switch dopo il blocco try...catch in 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!",
        _ => ""
    };
    

    Il metodo CheckForWin restituisce un'enumerazione che indica quale giocatore ha vinto il gioco o se la partita è terminata in pareggio. L'espressione switch imposterà il campo winnerMessage in modo appropriato se si verifica uno stato di fine del gioco.

    Ora, quando si gioca e si raggiunge uno scenario di fine del gioco, vengono visualizzati questi indicatori:

    Screenshot che mostra il pulsante per reimpostare il gioco.

Riepilogo

Sono state acquisite numerose informazioni su Blazor ed è stato sviluppato un piccolo gioco. Ecco alcune delle competenze apprese:

  • Creazione di un componente
  • Aggiunta del componente alla home page
  • Uso dell'inserimento delle dipendenze per gestire lo stato di un gioco
  • Implementazione dell'interattività del gioco con i gestori eventi per posizionare le pedine e reimpostare il gioco
  • Scrittura di un gestore errori per segnalare lo stato del gioco
  • Aggiunta di parametri al componente

Il progetto creato è un gioco semplice ma si può fare molto di più a partire da questo. Si è pronti a sfidarsi ulteriormente per migliorarlo?

Problematiche

Prendere in considerazione le attività seguenti:

  • Per ridurre le dimensioni dell’app, rimuovere il layout predefinito e le pagine aggiuntive.
  • Migliorare i parametri del componente Board in modo che sia possibile passare qualsiasi valore di colore CSS valido.
  • Migliorare l'aspetto degli indicatori con layout HTML e CSS.
  • Introdurre effetti audio.
  • Aggiungere un indicatore visivo e impedire l'uso di un pulsante di rilascio quando la colonna è piena.
  • Aggiungere funzionalità di rete in modo da poter giocare con un amico nel suo browser.
  • Inserire il gioco in un'applicazione .NET MAUI con Blazor per poter giocare sul telefono o sul tablet.

Buon divertimento!