练习 - 游戏逻辑

已完成

在本练习中,我们要将游戏逻辑添加到应用,以确保最终获得功能齐全的游戏。

为了帮助本教程与 Blazor 教学保持一致,我们提供了名为 GameState 的类,其中包含管理游戏的逻辑。

添加游戏状态

我们将 GameState 类添加到项目,然后通过依赖项注入将其作为单一实例服务提供给组件。

  1. GameState.cs 文件复制到项目的根目录中。

  2. 在项目的根目录中打开 Program.cs 文件,并添加此语句以在应用中将 GameState 配置为单一实例服务:

    builder.Services.AddSingleton<GameState>();
    

    现在,可以将 GameState 类的实例注入组件 Board

  3. Board.razor 文件的顶部添加以下 @inject 指令,以将游戏的当前状态注入组件:

    @inject GameState State
    

    现在,我们可以开始将 Board 组件连接到游戏状态。

重置状态

首先,当 Board 组件首次出现在屏幕上时,我们来重置游戏的状态。 我们将添加一些代码,以在初始化组件时重置游戏的状态。

  1. 通过调用 ResetBoardBoard.razor 文件底部的 @code 块内添加方法 OnInitialized,如下所示:

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

    当系统首次向用户显示棋盘时,将重置为游戏的初始状态。

创建游戏棋子

接下来,我们来分配 42 个可以操作的游戏棋子。 我们可以将游戏棋子表示为棋盘上 42 个 HTML 元素引用的数组。 可以通过分配一组具有列和行位置的 CSS 类来移动和放置这些棋子。

  1. 在代码块中定义字符串数组字段以保存游戏棋子:

    private string[] pieces = new string[42];
    
  2. 将代码添加到 HTML 部分,以在同一组件中创建 42 个 span 标记,分别对应每个棋子:

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

    完整代码应如下所示:

    <div>
        <div class="board">...</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();
        }
    }
    

    这会将一个空字符串分配给每个游戏棋子范围的 CSS 类。 CSS 类的空字符串会阻止屏幕显示游戏棋子,因为未应用任何样式。

处理游戏棋子的放置

让我们添加一个方法来处理玩家在列中放置棋子的情况。 GameState 类知道如何为游戏棋子分配正确的行,并将棋子进入的行报告回来。 我们可以使用此信息来分配 CSS 类,这些类表示玩家的颜色、棋子的最终位置和 CSS 放置动画。

我们将此方法称为 PlayPiece,它所接受的输入参数会指定玩家已选择的列。

  1. 将此代码添加到我们在上一步中定义的 pieces 数组下方。

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

PlayPiece 代码的作用如下:

  1. 我们告诉游戏状态在提交的名为 col 的列中放入一枚棋子,并捕获该棋子落入的行。
  2. 然后,我们可以定义要分配给游戏棋子的三个 CSS 类,以标识当前正在操作的玩家、放置该棋子的列以及该棋子所放入的行。
  3. 方法的最后一行将这些类分配给 pieces 数组中的游戏棋子。

如果查看提供的 Board.razor.css,则会找到与列、行和玩家轮次匹配的 CSS 类。

由此产生的效果是游戏棋子被放置在列中,并在调用此方法时经过动画处理,放入最底部的行。

选择列

接下来,我们需要放置一些控件,允许玩家选择列并调用我们的新 PlayPiece 方法。 我们将使用字符“🔽”来指示可以在此列中放入一枚棋子。

  1. 在起始 <div> 标记上方,添加一行可单击的按钮:

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

    @onclick 属性指定了单击事件的事件处理程序。 但如果要处理 UI 事件,需要使用交互式呈现模式呈现 Blazor 组件。 默认情况下,Blazor 组件是从服务器以静态方式呈现的。 可以使用特性 @rendermode 将交互式呈现模式应用于组件。

  2. 更新 Home 页面上的 Board 组件以使用 InteractiveServer 呈现模式。

    <Board @rendermode="InteractiveServer" />
    

    InteractiveServer 呈现模式会通过与浏览器的 WebSocket 连接从服务器处理组件的 UI 事件。

  3. 使用这些更改运行应用。 现在,它的外观应如下所示:

    Screenshot of Connect Four board.

    更好的是,当我们选择顶部的一个放置按钮时,可以观察到以下行为:

    Screenshot of Connect Four animation.

进展很大! 现在我们可以将棋子添加到棋盘。 GameState 对象的智能程度可以让它在两个玩家之间来回切换。 继续选择更多放置按钮并观察结果。

获胜和错误处理

现在,如果你来体验所配置的游戏,你会发现当你尝试在同一列中放置过多棋子时,以及当一个玩家赢得游戏时,它会引发错误。

我们来向棋盘添加一些错误处理和指示信息,让当前状态更加清晰。 我们在棋盘上方和放置按钮下方添加一个状态区域。

  1. nav 元素后面插入以下标记:

    <nav>...</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>
    

    使用此标记,我们可以显示以下项的指示器:

    • 宣布游戏获胜者
    • 允许我们重新开始游戏的按钮
    • Error messages
    • 当前玩家的轮次

    让我们填写一些逻辑来设置这些值。

  2. 在棋子数组后面添加以下代码:

    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;" : "";
    
    • CurrentTurn 属性是根据 winnerMessage 的状态以及 GameStatePlayerTurn 属性自动计算的。
    • ResetStyle 是根据 WinnerMessage 的内容计算的。 如果出现 winnerMessage,我们会在屏幕上显示重新开始的按钮。
  3. 我们来处理操作棋子时出现的错误消息。 添加一行以清除错误消息,然后使用 try...catch 块包装 PlayPiece 方法中的代码,以在发生异常时设置 errorMessage

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

    我们的错误处理程序指示器非常简单,它使用 Bootstrap CSS 框架在危险模式下显示错误。

    Screenshot of Your game so far, with a board and pieces.

  4. 接下来,我们添加重新开始游戏时按钮触发的 ResetGame 方法。 目前,重启游戏的唯一方法是刷新页面。 使用此代码,我们可以停留在同一页上。

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

    现在,我们的 ResetGame 方法具有以下逻辑:

    • 重置棋盘状态。
    • 隐藏指示器。
    • 将棋子数组重置为包含 42 个字符串的空数组。

    此更新让我们能再次玩这个游戏,现在棋盘上方会显示指示信息,告诉我们玩家的轮次并最终完成游戏。

    Screenshot displaying game over.

    我们仍然处于无法选择重置按钮的情况。 我们来在 PlayPiece 方法添加一些逻辑,以检测到游戏结束的情况。

  5. 我们在 PlayPiece 中的 try...catch 块后添加 switch 表达式,从而检测游戏是否存在胜者。

    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!",
        _ => ""
    };
    

    CheckForWin 方法返回一个枚举,该枚举报告赢得游戏的玩家或游戏平局。 如果出现游戏结束的状态,则此切换表达式将相应设置 winnerMessage 字段。

    现在,当我们玩到游戏结束的状态时,会显示以下指示信息:

    Screenshot showing to Reset game.

总结

我们学习了很多有关 Blazor 的知识并构建了简单的小游戏。 以下是我们学到的一些技能:

  • 创建组件
  • 将组件添加到主页
  • 使用依赖项注入来管理游戏的状态
  • 使游戏与事件处理程序交互,以放置棋子并重置游戏
  • 编写错误处理程序以报告游戏状态
  • 向组件添加参数

我们构建的项目是一个简单的游戏,你还可以借助该框架实现更多内容。 想体验一些进行改进的挑战?

挑战

请考虑以下挑战:

  • 移除应用中的默认布局和额外页面,以减小其大小。
  • 改进 Board 组件的参数,以便可以传递任何有效的 CSS 颜色值。
  • 使用一些 CSS 和 HTML 布局美化指示器外观。
  • 引入声音效果。
  • 添加可视指示器,并防止在列已满时使用放置按钮。
  • 添加网络功能,以便你可以在其浏览器中和好友玩游戏。
  • 使用 Blazor 应用程序将游戏插入 .NET MAUI,在手机或平板电脑上玩游戏。

祝你编码快乐,玩得开心!