最近在做数据结构的课程设计,选择了难度最大的黑白棋开发,因为已经决定使用纯C进行开发,所以之前朋友们说使用MFC开发就果断不去做了。网上有很多黑白棋的源码,不过大部分C语言的源码不是调用graphic库开发就是使用C++进行MFC开发,极少有像我这样用着纯C调用着Windows API进行开发的例子,所以既然自己痛苦了那么久,如果以后有人也想和我一样痛苦,我这里分享一下我的经验,就算不能从技术上帮到他,也能从心理上安慰一下他。废话不多说,直接来谈我的设计吧。

实现方式:

因为上次的C语言课设我就使用了Graphics库进行了dos图形界面的实现,所以这次数据结构的实验我想使用不同的实现方式,就采取了纯C调用SDK函数进行设计windows程序,但我还没有形成完整的面向对象的思想,所以在一开始就放弃了使用MFC编写程序。其实MFC也就是把windows的API用class进行封装了再使用,在网上能找到很多使用MFC写的黑白棋程序,但极少能有使用纯C直接调用windows API写的程序,我想这一点应该是一个突破。当然我并不是说使用MFC写程序不如SDK编程,只是为了将来更好的学习MFC,眼下我有必要掌握SDK编程,这样可以帮助我更加了解MFC Classes的结构,而MFC没有提供的API功能自己可以调用API来实现,当MFC Classes不合我的需求时我可以方便的更改其Class。总之最后我决定采取SDK编程。

知识储备:

因为之前我编写windows程序的经历就只有写过一个对话框程序的俄罗斯方块,那个逻辑相比黑白棋要简单很多,所以这次其实我还是得从头开始学习编写windows程序。我需要了解事件驱动的过程,需要了解windows一些基本的API 功能,还需要了解windows的消息处理机制,同时我还需要保持头脑清醒去设计我的程序逻辑,当然,这些知识大部分都能在MSDN中查询到,所以这个暑假MSDN我看了很久。

难点疑问:

因为在windows程序的编译器环境下调试起来比一般的程序调试要困难,虽然断点可以随便打,但想要拦截消息却不是那么容易,而且由于windows编程经验不足,导致在编程时走了很多弯路,本来可以不发送消息的地方我还特意显示的调用API发送消息,比如绘制主窗口的地方,当窗口被移动或是被挡住时,程序会自动向窗口句柄发送WM_PAINT消息,因为之前不知道这点,在程序设计时自己显示调用函数发送这个消息,结果导致界面不停闪烁。这类问题很多,相信老师明白我说的意思。相比较而言黑白棋逻辑的设计反倒显得简单点,因为网上有很多资料可供查阅,光黑白棋的下子算法就有一大堆,什么基本搜索算法,α-β剪枝算法,主要变化搜索算法,Zobrist散列算法,MTD(f)算法,迭代加深搜索算法等等,有很多这方面的资料可以查阅,但同时我也看到了我的不足,对于这些算法,看虽然看的懂,但让我自己设计恐怕无法设计出这些巧妙的算法,还有些算法我甚至现在无法明白究竟是什么意思,比如模拟退火算法,神经网络算法等等。数据结构就是算法的基础,可见我还有很多需要学习的地方。

资源设计:

本程序是windows下的程序,必然需要寻找资源,如图片,声音等等。这次我在网上下载了大量的黑白棋图片和声音资源,最终采取了一组黑白棋的资源,用VC++的资源编辑器编辑进了工程:

clip_p_w_picpath002

然后我自己创建了多个对话框和菜单也作为资源进行编辑:

1、关于黑白棋的对话框:

clip_p_w_picpath004

2、开局选项对话框:

clip_p_w_picpath006

3、进入英雄榜对话框:

clip_p_w_picpath008

4、帮助对话框:

clip_p_w_picpath010

5、英雄榜对话框:

clip_p_w_picpath012

6、设置对话框:

clip_p_w_picpath014

7、输棋对话框:

clip_p_w_picpath016

8、赢棋对话框:

clip_p_w_picpath018

9、平棋对话框:

 

clip_p_w_picpath002[4]

10、菜单:

clip_p_w_picpath004[4]

模块设计:

我对这个黑白棋程序做过两次模块设计,第一次我分了4个模块,主模块,绘图模块,逻辑控制模块,结束处理模块,后来我发现这样的设计太过于拘泥于软件成型的过程,而且我的程序不是一个非常大的工程,整个程序只有约2000行代码左右,所以我省去了前期的模块设计,直接进行编码,最终只有一个main.c的主模块程序,其他的逻辑功能实现全部交由main.h来实现,在global.h中定义了全局变量,resource.h文件是资源编辑器自动生成的文件,记录了各种资源的ID宏定义。

clip_p_w_picpath006[4]

AI设计:

电脑黑白棋最核心的部分就是搜索算法,它实际反映了黑白棋程序的算棋过程。搜索算法的好坏,将直接影响着程序的算棋速度和棋力。下面我们就从最基本的搜索算法开始。

对于图1所示的局面,白棋有三步棋可下:D6、F4、F6,黑白棋程序将如何找出最佳的棋步呢?

clip_p_w_picpath007
图1 白先,三步棋可下

其实程序的算棋过程和人类棋手很相似,它先对当前局面所有能下的棋步进行搜索,计算出每种棋步变化之后的局面,并根据各种局面结果,从中找出对自己最有利的一步棋。我们先来看看程序算一步棋的情况。在图1中,如果白棋分别下D6、F4、F6后,将形成图2、图3、图4三种局面。

clip_p_w_picpath008[4]
图2 黑先,估值为0

clip_p_w_picpath009
图3 黑先,估值为+6

clip_p_w_picpath010[4]
图4 黑先,估值为0

程序将对这三个局面分别进行评估,看看局面是优势还是劣势?这个局面评估过程一般由估值函数来完成。估值函数将根据盘面上棋子的分布情况,给出一个表示局面优劣程度的评估数值,这就称为估值。一般来说,估值为正时,表示局面处于优势,正值越大,优势越大;反之,负值表示劣势。例如,图3的局面对于黑棋而言略占优势,程序可能会给出+6的估值;而对于图2和图4,由于局面对双方均势,估值都是0。由于局面的优劣是相对的,对于一方来说是优势的局面,换从另一方角度来看就会是完全相反的结果。

估值可以有不同的取值范围,许多程序的估值是取值[-64, +64],以便直观地表示对终局比分的评估,如“估值为+6”则表示当前局面具有胜6子的优势。

通过对这三步棋所形成的局面进行评估,程序就可得到各步棋的估值(见图5)。由于这时下棋方是白棋,F4这步棋的估值就成了负值。换句话说,F4这步棋对白棋不利。

clip_p_w_picpath011
图5 白先,三步棋的估值

很显然,面对各种可能的棋步及其估值,下棋方最终会选择估值最大、也就是对自己最有利的那步棋。因此,白棋会选择下D6或F6,而不会下F4。程序的搜索过程就是将估值极大化,从而找出最佳棋步。

本程序的AI我设计为三个等级,第一个等级的AI采用的是最简单的估值函数,使用的是模板估值,而且模板是固定的,棋盘上各个地方的值是确定的,所以这个等级的AI没有随机性,有确定的走法,人很容易就能够赢了它;第二个等级的AI采用的是翻转能够赢得棋子最多的那步落子,如果出现有多处翻转的棋子一样多的情况就设置随机量,从中任选一个。这样设计的棋力也是十分低的,因为了解黑白棋的人都知道,黑白棋的关键在于谁先能抢占到4个角落,因为角落一旦被抢到就不会再改变颜色了,所以角落的值应该最高,还有边的分值应该也很高,而且边的各处分值也应该不一样,这在我的估值模板中有所体现。第三个等级的AI本来是想采用深层搜索多步来实现更加精确的估值,并采用α-β剪枝算法来进行优化的,但时间不足,所以没有完善它。

函数调用关系图:

clip_p_w_picpath002[6]

主界面:

clip_p_w_picpath002[8]

 

由于代码太长,所以我这里就不贴出来了,需要的朋友可以留下联系方式。如果正好有做过黑白棋设计的朋友可以和我交流下,我过段时间也想试下MFC设计。