练习 - 游戏逻辑

已完成

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

为了帮助本教程与 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">
        @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();
        }
    }
    

    这会将一个空字符串分配给每个游戏棋子范围的 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. 使用这些更改运行应用。 现在,它的外观应如下所示:

    四子棋棋盘的屏幕截图。

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

    四子棋动画效果的屏幕截图。

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

获胜和错误处理

如果在当前配置中玩游戏,则你会发现当尝试在同一列中放置太多棋子且一个玩家赢得游戏时,它会引发错误。

让我们通过向棋盘添加一些错误处理和指示器来明确游戏的当前状态。 在棋盘上方和放置按钮下方添加一个状态区域。

  1. 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 框架在危险模式下显示错误。

    屏幕截图:游戏目前的状态,以及棋盘和棋子。

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

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

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

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

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

    显示游戏结束的屏幕截图。

    我们仍然处于无法选择重置按钮的情况。 让我们在 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 字段。

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

    显示重新开始游戏的屏幕截图。

总结

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

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

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

挑战

请考虑以下挑战:

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

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