欢迎访问:常州市武进区嘉泽中心小学网站 !今天是:
栏目列表
您现在的位置是:首页>>教师>>计算机技术>>程序设计>>游戏开发>>文章内容
解剖大象的眼睛——中国象棋程序设计探索(九)
发布时间:2008-11-20   点击:   来源:本站原创   录入者:黄晨
 
() 实用程序片段
 
  ElephantEye的源程序包里有个<utility>目录,其中包含了很多实用程序片段,不仅仅是针对象棋程序的。由于程序片段中没有中文注释,所以这里作一下简要的介绍。
 
10.1 Visual Basic下的原子语句(atom.cpp/atom.bas)
 
  ElephantEye的部分代码可编译成CCHESS.DLL,成为界面程序ElephantBoard的一部分。然而由于Visual Basic处理数据结构的能力很弱,调用CCHESS.DLL时需要调用一些低级语句(例如QuickBasicVARPTRMKICVI等在Visual Basic中已经淘汰的语句),为此ElephantEye提供了这些原子,以保证数据结构使用的便利。
  在Visual Basic调用API函数的参数传递过程中,注意事项有:
  (1) 指针型变量用ByRef声明,自定义结构只能用ByRef(引用方式)传递参数;
  (2) 如果自定义结构用ByVal(传值方式)传递参数,则必须把它们转化为Visual Basic的内部类型;
  (3) Visual Basic的字符串类型(Windows内部类型是BSTR)必须用ByVal传递参数,Visual Basic会自动将其转化为LPCSTR类型;
  (4) API函数返回LPCSTR类型时,在Visual Basic下必须假定为Long类型。
  我们最感兴趣的是最后一条,LPCSTR类型的字符串由<atom.bas>中的MkBStr函数转换为Visual Basic的字符串类型(BSTR)。例如在Visual Basic下调用开局编号分析器ECCO.DLL,调用方式如下:
 
Declare Function EccoIndex Lib "ECCO.DLL" Alias "_EccoIndex@4" (ByVal FileMoveStr As String) As Long ' ECCO.DLL中的EccoIndex函数在Visual Basic下的接口
Declare Function EccoOpening Lib "ECCO.DLL" Alias "_EccoOpening@4" (ByVal EccoIndex As Long) As Long ' ECCO.DLL中的EccoOpening函数在Visual Basic下的接口
EccoStr = MkL(EccoIndex("C2.5N8+7")) ' 获得ECCO编号的字符串,这里将返回"B05"
OpeningStr = MkBStr(CvL(EccoStr)) ' 获得上述编号对应的开局名称,这里将返回"中炮对进左马"
 
10.2 汇编语言(x86asm.h)
 
  汇编语言是为了弥补C语言的不足,<x86asm.h>主要提供以下两方面的功能:
  (1) 操控互斥(Mutex)变量的原子语句,有IncrementDecrementExchange等,用于多线程的调度;
  (2) 32位和64位整数的乘除法和位移,有LongMulDivLongMulMod等,解决了“Int64 = Int32 x Int32”等计算问题。
  尽管WindowsUnix也有相应的库函数来支持这些功能,但是汇编语言通常以内联的形式调用,所以速度上会有优势。
 
10.3 空转(idle.h)
 
  在程序空闲期间,应该把CPU资源交还给系统,这时就要调用空转函数Idle(),也可以直接调用WindowsUnix下都的休眠函数(Sleep()usleep())
 
10.4 随机数(longrand.h)
 
  ElephantEye在使用随机数时,没有用<stdlib.h>中的rand()函数,是出于以下几点考虑的:
  (1) rand()函数的有效位数只有15(最大数为0x7fff),这远不能满足程序的需要,用多个rand()函数来拼凑只是权益之计;
  (2) rand()函数的可靠性无法得到保障,因为我们不知道它的算法;
  (3) 如果以Zobrist键值的形式来描述开局库中的局面,那么每次给定相同的种子以后,产生的Zobrist键值都必须相同。但各种C语言的编译器中给出的rand()函数,我们无法确定其工作原理是否相同,因此也就不知道它们是否会产生相同的Zobrist键值了。
  ElephantEye的随机函数包含在<longrand.h>中,它采用了“乘同余法”,公式是这样的:
Ij+1 = (a x Ij) mod m
  该公式可以用<x86asm.h>中的LongMulMod()函数来实现。这个算法在1969年首先由LewisGoodmanMiller提出,这里am的推荐值是:
a = 75 = 16807
m = 231 - 1 = 2147483647
  这就使得随机数I的范围从1231 - 2(31),初始化种子数时,可以用1231 - 2中的任意一个数。ElephantEye在产生Zobrist键值时,用1作种子数,而用作其他用途时,用time()作种子数。
 
10.5 计时(timer.h)
 
  C语言里有很多计时函数,如<time.h>中的time()ftime()clock()等,笔者认为能达到精确计时功能的只有ftime(),为此针对该函数设计了<timer.h>,在调用时可以避开对btime结构的直接处理。计时器是类型为TimerStruct的对象,成员函数Init()把当前时间写入该计时器,而GetTimer()返回当前时间和该计时器的差值(毫秒)
 
10.6 管道的行输入和行输出(pipe.h/pipe.cpp)
 
  ElephantEye是用“轮转”方式来接收行输入的(1.1版本开始)。轮转不需要增加线程,只需要每隔一定时间让搜索过程去处理一下接收命令的子过程就可以了。轮转的操作原理在每个操作系统下都是一样的,这种方式甚至能在不支持多任务的DOS系统下运行。在轮转中调用ReadFile()(Windows的标准库函数)read()(UNIX的标准库函数)函数时,使用起来要非常小心,通常需要调用检测输入的函数,只有确认标准输入句柄有输入时,才调用读入信息的函数。然而就检测和读取输入信息的操作而言,程序严重依赖于操作系统,有兴趣的读者可注意<pipe.cpp>中的“#ifdef _WIN32”语句,比较一下Windows代码和UNIX代码的区别。
  如果输入输出的对象不是标准输入输出句柄而是程序,那么必须用管道来连接子进程的标准输入输出句柄和主进程的控制句柄。用CreatePipe()(Windows的标准库函数)pipe()(UNIX的标准库函数)建立管道时,务必要明确输入输出的方向,一个管道有两个句柄,数据总从后一个句柄输入(称为“写入端”),从前一个句柄输出(称为“读出端”),因此可以用两种方式来使用管道:
  (1) 写入端定向为子进程的标准输入句柄,读出端用来向子进程发送指令;
  (2) 写入端用来接收子程序的反馈信息,读出端定向为子进程的标准输出句柄。
  因此,如果象棋程序的界面要控制引擎,必须建立2个管道,产生4个句柄,其中2个定向给引擎,2个用来发送和接收数据,操作非常烦琐。好在<pipe.cpp>可自动完成这一切操作,具体应用可参阅UCCI2QH(UCCI引擎的浅红象棋适配器)UCCILEAG(UCCI引擎联赛模拟器)的源程序。
 
10.7 位搜索(bitscan.h/bitscan.cpp)
 
  位搜索(BitScan)主要用于位棋盘的操作,Intel处理器提供了两个指令,即搜索最低位(BSFBitScan-Forward)和搜索最高位(BSRBitScan-Reverse)的操作,位棋盘的国际象棋程序Crafty用了FirstOne()LastOne()两个函数,就是专门调用这两个指令的。然而很多试验表明,这两个指令是非常耗时的,而“查表代替计算”会比直接用汇编指令快得多。
  <bitscan.cpp>为查表计算分配了两个64K的表,直接获得16位数的位搜索结果,32位数则分解成两个16位数再分别查表,同时该模块还包含位计数(BitCount)的操作,同样用了64K的表。用<bitscan.h>提供的BitScanForward()BitScanReverse()函数做位搜索操作,耗时比直接调用汇编指令少一半,但切记使用查表函数前务必用BitScanInit()函数初始化。
 
10.8 96位位棋盘(bitboard.h)
 
  尽管ElephantEye没有使用位棋盘,但位棋盘(BitBoard)仍然是一个富有活力的数据结构,为此ElephantEye的源程序包里保留了位棋盘结构的描述,即<bitboard.h>。位棋盘的赋值、比较、位操作(与或非等)、位移、位搜索都由该文件来定义(定义为内联函数的运算符),另外还有为“折叠位棋盘”(专门处理蹩马腿和塞象眼的算法)而设计的操作,即CheckSum()Duplicate()函数。
附件:
    关闭窗口
    打印文档
    账号登录
    保持登录 忘记密码?
    账号与武进教师培训平台同步