分类:
2008-09-17 11:07:47
前言
这是一个用C#写的翻转棋游戏。
背景
我原来写这个程序只是为了常规上学习C#和.NET编程而做的一个练习。翻转棋,大家都知道,是一个比较流行而且比较有趣的游戏,原理和规则都很简单。这使得它成为学习一种新的语言或工具时的一个很好的练习选择。
开始时的练习虽然可以用来当作比赛的游戏,但还是缺少一些电脑棋盘游戏一般的特征,比如撤消走过的步骤,因此,积累了一些.NET的经验以后,就将它升级了。升级后增加了一些新的功能,而且改进了原来的图形界面和游戏AI。
使用代码
编译源文件并运行Reversi.exe就可以开始玩游戏了。你可以通过菜单和工具栏进行设置,以实现不同选项下的玩法。如在游戏过程中调整窗口大小,改变颜色和电脑对换角色等。
当你退出游戏时,你会发现程序建立了一个叫Reversi.xml的文件。这个文件是用来保存各种设置比如窗口大小、位置和玩家的统计数据等,当你再次运行游戏时,它会将这些设置重新加载进来。
帮助文件
帮助文件跟源代码放在一起,你可以在help files子文件夹中找到。为了使帮助文件可用,只需将Reversi.chm文件拷贝到Reversi.exe所在目录即可。你可以不这样而直接运行程序,但这样当你点击Help Topics时,就会出现一个错误,说文件找不到。
所有用于建立help file的源文件都放在那个子目录中,你可以编辑并用重新编译它。
游戏 AI
代码中比较精华的部分是关于计算电脑和玩家的移动步数部分,因此值得探讨一下。程序使用了标准的min-max look-ahead算法来决定电脑最好走哪步。Alpha-beta 裁剪是用来提高look-ahead搜索效率的,如果你不熟悉min-max算法和alpha-beta裁剪,你可以到Google上搜索一下,会有很多信息和例子。
当然,在游戏中有太多的走法,这需要穷举法,它会花很长时间列举出所有可能的走法。一个例外是在游戏要结束时,在10或12格周围剩下很少的方格。在这里,要做一个完全的搜索,才最有可能找出最好的移动路径。
但是大多数情况下,look-ahead搜索的深度要限定在一定的数量上,这要根据玩家的难易级别设置。所以对于每一个移动路径和移动步数,游戏棋盘都必须要测算一下来决定哪个玩家最有可能赢得比赛。这个测算是通过利用下列标准计算行列来实现的:
得分增加的幅度也是根据玩家设置的游戏难易级别来定的。有一个RmaxRank常量来代表游戏到了最后阶段。它的值被设为RSystem.Int32.MaxValue-64.这可以保证在游戏的最后任何一个移动都比平时有比较高的级别。
现在这个游戏虽然敌不上高智商的玩家,但对一些一般的对手,它会走的很漂亮。另外,在 上我们可以找到很多游戏策略。
游戏的组成
Board 类
Board 类描述了游戏的棋盘。 它用一个二维数组来记录每一个棋盘方格的状态,它只能是下面的在类中已经定义好的常量之一:
程序中共有两个构造函数,一个用来创建一个新的、空的棋盘,另一个用来保存游戏退出时的棋盘状态。
MakeMove()方法是public的,用来增加棋盘上的一个方格,IsValidMove()用来验证玩家走的是否合法。如果玩家无路可走的时候,HasAnyValidMove()就会返回false。
还要记录每个玩家的方格数量,包括总的方格数量、边界的方格数量和不同颜色的不会被攻击的棋子的数量。
移动结构
在主类ReversiForm中,有两个结构,用来存储移动的步,两个结构中都包含了一个行索引和一个列索引,分别与棋盘方格相对应。
ComputerMove结构是为计算机AI的,有一个rank成员变量存储移动的位置。用来记录移动步的好坏程度,这是在look-ahead搜索过程中决定的。
MoveRecord结构是用来存储游戏期间每个移动步骤的信息的。允许撤消行为,这样的一个结构数组用来记录每个回合的棋盘状态。一个移动记录包含了移动之前棋盘状态,还有一个值来记录下一步该哪个玩家走。这样的一个结构数组就可以记录每个玩家的移动情况,允许游戏重新设回原来的任何一个时间的状态。
RestoreGameAt()方法来处理游戏从哪个地方从新开始,这意味着可以将游戏重新设回原来的任何一个时间的状态。而游戏界面上的菜单和工具栏中只提供撤消一步的功能,将来的版本可能会增加一个列表,列出走过的每个时间点,玩家可以通过点击列表中的时间点来一下子返回到某个历史状态。
图形用户界面
游戏棋盘
SquareControl控件用来描述棋盘上的方格,控件包含了方格的信息和它的状态(empty or a black or white disc)包含带亮圈的活动棋子。
显示棋子
每个棋子都是动态画上去的,基本的形状是一个圆行带有阴影的3D效果,棋子的大小是根据棋盘上的方格大小决定的。通过这种方式代替静态图象,棋盘也可以动态调整大小以适应窗口的尺寸。
Click事件是在ReversiForm中处理的,它允许用户走一步棋(假设是合法移动)。同样,MouseMove 和 MouseLeave 事件是用来更新棋盘显示的,进而用亮色标出哪些方格是可以走棋子的。
移动的动作
棋子的翻转动作是利用SquareControl 类中的记数器和 System.Windows.Forms.Timer来完成的。因为,这是一个操作系统控制的线程,它可以定期的触发事件来等待你的程序去响应。
当记时器触发事件的时候,AnimateMove()方法将被响应,来更新方格数量并重画。这个动作主要包含改变棋子形状(从正圆形到扁圆形),然后再以相反的颜色回到正圆形。翻转的平滑程度和速度取决于初始值的大小(由常量SquareControl.AnimationStart设定)和记时器的频率(由常量 animationTimerInterval设定)。
玩游戏
下列变量用来处理游戏的Play:
// 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).
}
大多数游戏的Play都是事件驱动的,因此gameState允许不同的事件handler决定适当的动作。比如说,当用户点击棋盘上的一个方格时,SquareControl_Click()被调用,如果游戏状态是InPlayerMove,则棋子将被放在那个方格中。但是,如果游戏正处于其它状态,就是说没有到用户可以操作的回合,则点击将被取消。
同样,如果用户点击工具栏中的"Undo Move"按钮,我们想检查一下游戏的状态,看看在撤消时都做了什么。比如,如果游戏状态是InMoveAnimation,活动的timer将暂停,而方格控件启动记数器。如果状态是InComputerMove,程序将会另起一个线程来进行look-ahead搜索。
程序流程
下面的图说明了游戏过程中的流程:
StartTurn() 将在游戏之初被调用,它负责评测游戏的形式并为下一步移动做些设置。
它首先检查当前玩家的移动是否合法,如果不合法,它将切换到对方玩家并检查是否有合法移动。当双方都没有合法移动时,按规则,游戏就会结束。
另外,它也为当前玩家做些设置,如果当前玩家是用户,它就退出,玩家就可以通过点击鼠标来移动棋子。如果当前玩家是计算机,它将启动一个look-ahead搜索去找到最好的移动步骤。
用一个工作线程
因为look-ahead搜索是一个深度计算,所以它要在一个工作线程中完成。如果不是这样,那么在进行给计算机搜索最好走法时,主窗口将会不响应而死在那里。因此,StartTurn() 用来创建一个工作线程并且执行CalculateComputerMove()方法开始搜索。
在游戏棋盘对象中的Lock用来防止紊乱情况的发生。一个例子,MakeComputerMove() 和 UndoMove()方法都可以改变棋盘,如果用了Lock,则当一个方法即将执行,而另一个方法正在更新棋盘时,这个方法就必须等待,直到另一个方法更新完毕后并将Lock释放后才能执行。
当搜索到一个好的移动时,CalculateComputerMove() 方法就会做个回调来执行MakeComputerMove(),这个方法将得到一个lock并调用MakeMove()执行移动。
执行移动
MakeMove() 方法更新棋盘,并将棋子放在指定位置。它也做一些移动记录的维护和消除方格的亮色工作。
然后,如果animate选项被关闭,它就会调用EndMove()方法来切换currentColor并且通过调用StartTurn()来启动下一回合。
但是,如果animate选项打开,它就会设置成活动的,而不去做初始话工作,并启动animation timer。前面说过,timer会每隔几毫秒就去触发一个事件调AnimateMove()方法来更新显示并记数。最后,记数要结束的时候,AnimateMove()方法就会调用EndMove()来完成移动。
将来的升级
在计算机玩家的AI方面还有很大的升级空间,look-ahead算法还可以进一步扩展。另一个可以改进的地方是存储look-ahead树,这可以让搜索深度更深,因为程序每次都不会走同一个步骤。
程序升级纪录
文章原文链接地址: