65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的 Reversi

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (180投票s)

2003年8月1日

13分钟阅读

viewsIcon

1106260

downloadIcon

27068

C# 语言实现的 Reversi 游戏。

Sample Image - Reversi.gif

引言

这是用 C# 编写的 Reversi 游戏实现。

背景

我最初编写这个程序是为了练习 C# 和 .NET 编程。Reversi(也称为 Othello)是一款流行的游戏,它很有趣,只需要几个基本元素和简单的规则。这使得它成为学习新编程环境的良好选择。

结果是制作了一个基本可玩的游戏,但它缺少计算机棋盘游戏的一些更常见的功能,例如撤销移动的能力。因此,在积累了更多 .NET 经验后,是时候进行升级了。新版本增加了一些新功能,并改进了原始的图形和人工智能。

Using the Code

编译源文件并运行生成的 Reversi.exe 可执行文件即可玩游戏。您可以使用菜单或工具栏来调整各种选项和设置。尝试调整窗口大小、更改颜色或在游戏进行中与计算机交换角色。

您可能会注意到,程序在退出时会创建一个名为 Reversi.xml 的文件。此文件用于保存各种设置,例如游戏选项、窗口大小和位置以及玩家统计信息,这些信息将在程序再次运行时重新加载。

帮助文件

源代码中包含一个 Windows 帮助文件。它位于存档中的 help files 子目录中。为了使帮助文件可供程序使用,只需将 Reversi.chm 文件从该位置复制到 Reversi.exe 可执行文件所在的目录。您可以独立运行程序,但单击 Help Topics 选项会显示一条错误消息,提示找不到文件。

用于创建此帮助文件的所有源文件都包含在该子目录中。您可以使用 Microsoft HTML Help Workshop 来编辑和重新编译它。

关注点

源代码注释非常详细,但此处仍有必要提供一些总体概述。

游戏人工智能

代码的大部分与计算计算机玩家的走法有关,因此值得讨论。该程序使用标准的极小化极大搜索算法来确定计算机的最佳走法。使用 alpha-beta 剪枝来提高搜索效率。如果您不熟悉极小化极大算法和/或 alpha-beta 剪枝,可以在 Google 上搜索相关信息和示例。

当然,游戏中可能的走法序列太多,无法进行详尽的向前搜索,否则生成所有可能的走法组合将花费太长时间。例外情况是在游戏接近尾声时,剩余的空方格相对较少 - 大约十几个。此时,可以进行完整搜索,并找到对给定玩家而言结果最好的走法。

但在大多数情况下,向前搜索的深度必须限制在一定的回合数(基于游戏难度设置)。因此,对于搜索的每一系列可能的走法和应对走法,都必须评估由此产生的棋盘,以确定哪个玩家最终更有可能赢得比赛。这种评估是通过使用以下标准计算等级来完成的:

  • 弃权 - 让对手没有合法走法迫使她放弃回合,从而在连续走两步(或更多步)方面获得巨大优势。
  • 机动性 - 这是衡量您能做出多少合法走法与对手将有多少合法走法。与弃权类似,其思想是减少对手的选项,同时最大化您自己的选项。
  • 前沿 - 前沿棋子是与空方格相邻的棋子。拥有大量前沿棋子通常会给对手在后续回合中更大的机动性。相反,拥有较少的前沿棋子意味着对手以后机动性会受到限制。此分数反映了您前沿棋子与对手的比例。
  • 稳定性 - 角落棋子是稳定的,它们永远不会被包围。随着游戏的进行,其他棋子也会变得稳定。此分数反映了您稳定棋子与对手的比例。
  • 分数 - 这只是您棋盘上的棋子数量与对手棋子数量之差。

每个分数都有不同的权重(同样,基于游戏当前的难度设置)。通过将每个标准分数乘以其相应的权重并将这些值相加来为棋盘分配等级。大的负等级表示对黑方有利的棋盘,而大的正等级表示对白方有利的棋盘。因此,对于给定的一组可能的走法,计算机将选择一个导致最佳等级棋盘的走法。

名为 maxRank 的常量用于指示游戏结束。它被设置为 System.Int32.MaxValue - 64 的值。这确保任何导致游戏结束的走法都将始终比其他走法具有更高的等级(负或正)。

从系统最大整数值中减去 64 允许我们为等级加上最终分数,这样以 10 个棋子获胜的得分就比以 2 个棋子获胜的得分更高。这使得计算机玩家在获胜时最大化其分数(或在失败时最小化对手玩家的分数)。

目前的实现无法与更好的 AI 玩家匹敌,但它对弱小的人类对手(至少是这个弱小的人类对手)来说相当强劲。再次,在 Google 上搜索可以找到许多描述该游戏策略和 AI 方法的资源。

游戏组件

Board

Board 类表示游戏棋盘。它使用二维数组来跟踪每个棋盘格的内容,这些内容可以是类中定义的以下常量值之一:

  • 黑色 = -1
  • Empty = 0
  • 白色 = 1

提供了两个构造函数。一个创建新的空棋盘,另一个创建现有棋盘的副本。

它提供了公共方法,如 MakeMove(),它将一个棋子添加到棋盘上,翻转任何被包围的对手棋子。例如,IsValidMove() 可用于确定给定玩家的给定走法是否有效。HasAnyValidMove() 如果给定玩家无法做出任何合法走法,则返回 false

它还跟踪每个玩家的棋子计数,这些计数被计算机走法 AI 例程使用。这些计数包括总棋子数、前沿棋子数以及每种颜色的安全(或不可翻转棋子)棋子数。

走法结构

在主 ReversiForm 类中,定义了几个结构来存储游戏走法。两者都包含行和列索引对,对应于特定的棋盘格。

ComputerMove 结构用于计算机 AI。除了走法位置外,它还有一个 rank 成员。这用于跟踪在向前搜索期间确定的走法的好坏程度。

MoveRecord 结构用于存储游戏过程中进行的每一步的信息。为了实现撤销/重做功能,保留了这些结构的数组,以在每个回合跟踪棋盘。走法记录包含一个 Board,表示在进行特定走法之前的游戏棋盘,以及一个指示谁将进行下一步的指示值。在每个玩家进行走法时,会保留一个这些结构的数组,允许游戏重置到移动历史中任何一点的状态。

RestoreGameAt() 方法负责将游戏重置到特定的走法编号。虽然它可能允许游戏恢复到历史记录中的任何当前走法,但主窗体上的菜单和工具栏选项目前只提供一次撤销/重做或全部撤销/重做。未来的增强功能可能包括允许用户单击走法列表中的项目以将游戏恢复到相应的走法编号。

图形和用户界面

游戏棋盘

棋盘上的方格由一个名为 SquareControl 的用户控件表示。游戏棋盘显示中的每个方格都有一个这样的控件。该控件包含显示方格及其内容(空或黑白棋子)的信息,包括棋子的动画和任何高亮显示。

显示棋子

每个棋子都是动态绘制的。基本形状是带有高光和阴影的圆形,使其具有伪 3D 外观。形状的大小根据方格控件的当前大小进行缩放。通过这种方式渲染形状,而不是使用静态图像,可以动态调整棋盘大小以适应窗体窗口。

ReversiForm 中处理方格控件上的 Click 事件允许用户在特定方格进行走法(假设是合法走法)。同样,处理 MouseMoveMouseLeave 事件是为了更新棋盘显示,以在高亮显示合法走法或预览走法(当这些选项处于活动状态时)。

走法动画

棋子翻转动画是通过 SquareControl 类中定义的计数器以及 System.Windows.Forms.Timer 来实现的。基本上,这是一个由操作系统控制的线程,它会定期引发您的窗体应用程序可以响应的事件。

走法完成后,如果启用了走法动画选项,则每个受影响的方格控件的计数器将被初始化,并激活计时器。主窗体的 AnimateMove() 方法在每次计时器滴答时被调用(见下文)。此方法更新方格计数器并重绘其显示。动画基本上包括将棋子形状从圆形变为越来越细的椭圆形,然后再变回完全圆形,但颜色相反。动画的流畅度和速度取决于初始计数器值(由常量 SquareControl.AnimationStart 设置)以及计时器滴答的频率(由主窗体中的常量 animationTimerInterval 设置)。

游戏玩法

以下变量用于处理游戏过程中的进行:

// Game parameters.
private GameState gameState;
private int       currentColor;
private int       moveNumber;

moveNumber 应该不言自明。currentColor 指示当前轮到哪个玩家(黑或白)。gameState 设置为以下枚举值之一:

// Defines the game states.
private enum GameState
{
    GameOver,        // The game is over (also used for the initial state).
    InMoveAnimation, // A move has been made and the animation is active.
    InPlayerMove,    // Waiting for the user to make a move.
    InComputerMove,  // Waiting for the computer to make a move.
    MoveCompleted    // A move has been completed
                     // (including the animation, if active).
}

大部分游戏玩法是事件驱动的,因此 gameState 的使用允许各种事件处理程序确定要执行的适当操作。例如,当用户单击棋盘方格时,会调用 SquareControl_Click()。如果游戏状态为 InPlayerMove,则会在该方格进行走法。但如果游戏处于其他状态,则不是用户走法的回合,因此单击将被忽略。

同样,如果用户单击工具栏上的“撤销走法”按钮,我们需要检查游戏状态以查看在重置游戏到上一步之前是否需要执行任何操作。例如,如果状态是 InMoveAnimation,则需要停止动画计时器,并且方格控件需要重置其计数器和显示。或者,如果状态是 InComputerMove,则程序当前正在单独的线程中执行向前搜索(见下文),这需要被停止。

程序流程

下图说明了游戏过程中一般的程序流程:

Figure1

StartTurn() 在游戏开始时、由任一玩家走完一步后以及执行撤销或重做走法后被调用。它负责评估游戏情况并为下一步做准备。

它首先检查当前玩家是否可以做出合法走法。如果不能,它会切换到另一位玩家并检查该玩家是否有任何合法走法。当两个玩家都无法走棋时,按照规则,游戏结束。

否则,该函数将为当前玩家设置走棋的准备。如果当前玩家由用户控制,它将简单退出。然后用户可以通过单击鼠标指针在有效方格上或通过输入有效的列字母和行号来进行走法。这将调用 MakePlayerMove(),它会进行一些管理工作,然后调用 MakeMove() 来执行走法。如果当前玩家由计算机控制,它将启动向前搜索以找到最佳走法。

使用工作线程

由于向前搜索在计算上非常密集,因此它在一个工作线程中执行。如果不是这样,主窗体将在计算机计算其最佳走法时冻结并变得无响应。因此,StartTurn() 创建一个工作线程来执行 CalculateComputerMove() 方法并启动它。

使用 `lock` 保护主游戏棋盘对象,以防止竞态条件。例如,MakeComputerMove()UndoMove() 方法都修改游戏棋盘。两种方法都首先尝试对它进行锁定。因此,如果一种方法恰好在另一种方法正在更新棋盘时被调用,它将被迫等待,直到这些更改完成并且锁定被释放。

一旦找到走法,CalculateComputerMove() 方法会进行回调,在主窗体中运行 MakeComputerMove()。此方法获取对棋盘的锁定,并调用 MakeMove() 来执行走法。

执行走法

MakeMove() 执行棋盘的实际更新,在指定位置放置新的棋子。它还对撤销/重做走法历史进行一些维护,并移除任何方格高亮。

然后,如果禁用了走法动画选项,它将简单地调用 EndMove(),后者将切换 currentColor 并通过调用 StartTurn() 来开始下一个回合。

但是,如果启用了动画选项,它将初始化要动画化的棋子并启动动画计时器。如前所述,计时器每隔几毫秒调用一次 AnimateMove(),更新显示并每次递减动画计数器。最终,计数器将达到其终点,AnimateMove() 将调用 EndMove() 来完成走法。

未来的增强

计算机玩家 AI 仍有很大的改进空间。向前搜索算法可以补充开局棋谱或预先排好等级的角落和边缘模式集。它可以被制作为利用选择性加深,其中搜索深度可以针对通常对游戏影响更大的走法进行扩展,例如靠近角落的走法。另一个改进是存储向前搜索树。这将允许它以更深的深度进行搜索,因为程序不必每次都重新生成相同的走法。

历史

  • 2003年8月1日 - 版本 1.0
    • 初始版本。
  • 2005年9月16日 - 版本 2.0
    • 增强的图形显示。
    • 添加了无限次撤销/重做。
    • 改进了计算机玩家 AI。
    • 纠正了线程问题。
    • 扩展了帮助。
© . All rights reserved.