|
五子棋是一种受大众广泛喜爱的游戏,其规则简单,变化多端,非常富有趣味性和消遣性。这里设计和实现了一个人机对下的五子棋程序,采用了博弈树的方法,应用了剪枝和最大最小树原理进行搜索发现最好的下子位置。介绍五子棋程序的数据结构、评分规则、胜负判断方法和搜索算法过程。
/ Q& f1 c6 G8 P( h/ E* l6 f$ g+ Y ~
一、相关的数据结构 - ]8 U- |) J) k1 Q4 g* H
关于盘面情况的表示,以链表形式表示当前盘面的情况,目的是可以允许用户进行悔棋、回退等操作。
# C' t0 b& l1 k CList StepList;
4 }$ n# m- L4 J2 v7 h! h O 其中Step结构的表示为: " T6 ~0 K; s2 V& M+ M
9 z# e# j* T% S6 f+ s6 f8 T- \
struct Step
/ w* ^; g$ X1 n4 G5 g3 g {
, A) Z& N2 j3 ~7 A int m; //m,n表示两个坐标值
, T& ?: R2 x! k. ~' }+ H( L- G3 Q$ |7 C int n; 1 ]# g3 n+ f; O* j6 p- i
char side; //side表示下子方 : ], d5 [2 |6 k8 l
}; ' m, z% j. F! d5 K; O
以数组形式保存当前盘面的情况, ! g4 a3 ^5 y! T2 Z
目的是为了在显示当前盘面情况时使用: 4 I: Y7 \, d( R8 g+ v
char FiveArea[FIVE_MAX_LINE][FIVE_MAX_LINE];
1 S$ z$ ]0 P9 D1 L% }
/ U/ s; A- R( a9 b& u0 T% T; k- g: w 其中FIVE_MAX_LINE表示盘面最大的行数。
4 U# a% P0 I$ q- O8 @, |: c' S* |; H. c$ \
同时由于需要在递归搜索的过程中考虑时间和空间有效性,只找出就当前情况来说相对比较好的几个盘面,而不是对所有的可下子的位置都进行搜索,这里用变量CountList来表示当前搜索中可以选择的所有新的盘面情况对象的集合: . E* g; k- [' z5 b2 O: Z
6 A7 r o: c# L& I$ B% i; m! A1 p% `
CList CountList; ) z x4 s8 }% V+ q7 w
其中类CBoardSituiton为: * }, ^6 J! c' X+ A; E
class CBoardSituation
4 I7 ], C% ^/ t2 R1 g9 \! m2 { {
0 R& I% Z. I' b8 j* E J CList StepList; //每一步的列表 {; [) a& e5 g) V6 W- R; r* h
char FiveArea[FIVE_MAX_LINE][FIVE_MAX_LINE]; ( A+ h! l/ Y q! c* h9 k, F! R5 @) r' Q7 H
struct Step machineStep; //机器所下的那一步
( K& J, l2 B/ B0 X; D double value; //该种盘面状态所得到的分数 ) k8 m. i. r- `! M' o5 y7 D
} 5 Z6 q3 {; A* B# O% {
' d1 {! q9 k4 M& S
二、评分规则 $ S. q9 n! L, U# G& H
对于下子的重要性评分,需要从六个位置来考虑当前棋局的情况,分别为:-,¦,/,\,//,\\ : r6 U5 X# h1 L! G
$ i9 U a" s) g- @/ B; L8 \ a& P5 @& V, g
实际上需要考虑在这六个位置上某一方所形成的子的布局的情况,对于在还没有子的地方落子以后的当前局面的评分,主要是为了说明在这个地方下子的重要性程度,设定了一个简单的规则来表示当前棋面对机器方的分数。 ! o/ W8 s W* r" _5 L) Z
0 V. l3 b6 ?% ]4 m- R% o0 r5 O3 h
基本的规则如下: ; p( Q0 P' L& F" o
( m$ Z! o! g; m3 r
判断是否能成5, 如果是机器方的话给予100000分,如果是人方的话给予-100000 分;
% u3 F8 _" A+ ^判断是否能成活4或者是双死4或者是死4活3,如果是机器方的话给予10000分,如果是人方的话给予-10000分; ' H; u b0 x( J, a& P8 ]
判断是否已成双活3,如果是机器方的话给予5000分,如果是人方的话给予-5000 分; ) S0 [9 P1 E: \# E. q2 e
判断是否成死3活3,如果是机器方的话给予1000分,如果是人方的话给予-1000 分;
# ^3 f+ S* ~, X0 u判断是否能成死4,如果是机器方的话给予500分,如果是人方的话给予-500分; 1 d$ @0 w$ h9 \( j
判断是否能成单活3,如果是机器方的话给予200分,如果是人方的话给予-200分;
' Q8 z8 c b H% O1 C! a& A: }! g, U判断是否已成双活2,如果是机器方的话给予100分,如果是人方的话给予-100分;
$ m+ n( d5 d N% M9 r判断是否能成死3,如果是机器方的话给予50分,如果是人方的话给予-50分;
9 V$ u% P% g' G' d' ~" |判断是否能成双活2,如果是机器方的话给予10分,如果是人方的话给予-10分; 6 F& Y5 p( k5 [7 n2 F4 [
判断是否能成活2,如果是机器方的话给予5分,如果是人方的话给予-5分;
1 E! Y Y) o( W+ h) f判断是否能成死2,如果是机器方的话给予3分,如果是人方的话给予-3分。
. m" X3 f+ h' }3 [
) E9 D: T4 K( F/ y 实际上对当前的局面按照上面的规则的顺序进行比较,如果满足某一条规则的话,就给该局面打分并保存,然后退出规则的匹配。注意这里的规则是根据一般的下棋规律的一个总结,在实际运行的时候,用户可以添加规则和对评分机制加以修正。 % \$ ~1 w8 F) Y& r H, a0 d
% ]/ ^; W f4 z. @3 `: B三、胜负判断 ' a* K4 F/ ~) e( A6 c; \( l
实际上,是根据当前最后一个落子的情况来判断胜负的。实际上需要从四个位置判断,以该子为出发点的水平,竖直和两条分别为 45度角和135度角的线,目的是看在这四个方向是否最后落子的一方构成连续五个的棋子,如果是的话,就表示该盘棋局已经分出胜负。具体见下面的图示: . z) ?. Q( L* ]0 F* T% Y$ p
; @# m* w) P9 E8 a1 l8 e
( o {- {, c0 L7 G. x' a四、搜索算法实现描述
$ g: M+ d; ~( Q2 T2 X 注意下面的核心的算法中的变量currentBoardSituation,表示当前机器最新的盘面情况, CountList表示第一层子节点可以选择的较好的盘面的集合。核心的算法如下:
3 \# R7 o, g; j6 W* @8 Jvoid MainDealFunction()
9 U1 S2 t% t9 C3 c+ U- }{
1 D$ ?$ M4 W W' `5 q: c* k7 @ value=-MAXINT; //对初始根节点的value赋值
6 ^! L6 `: a2 d1 j$ f7 bCalSeveralGoodPlace(currentBoardSituation,CountList);
m2 V; g6 |3 X//该函数是根据当前的盘面情况来比较得到比较好的可以考虑的几个盘面的情况,可以根据实际的得分情况选取分数比较高的几个盘面,也就是说在第一层节点选择的时候采用贪婪算法,直接找出相对分数比较高的几个形成第一层节点,目的是为了提高搜索速度和防止堆栈溢出。
4 O% `) S0 F2 R7 j# p2 n9 b8 epos=CountList.GetHeadPosition(); 9 j- V( {6 R( ^& ]. F' E$ v* _
CBoardSituation* pBoard;
7 z, t8 }$ P6 [2 j! u% Kfor(i=0;ivalue=Search(pBoard,min,value,0);
( e2 V/ \) F9 N/ D$ C5 L4 A9 Q: t+ l( P Value=Select(value,pBoard->value,max); * v& [- W; E- w$ c
//取value和pBoard->value中大的赋给根节点 ; X' q4 z1 j+ h( }
}
! D6 }+ I/ v; p) t0 L# W; Ifor(i=0;ivalue)
: E2 W0 r* ~4 X' l% {( w3 Y//找出那一个得到最高分的盘面 + O5 |9 T5 n9 W/ H& X! w& Q
{
. N. Y+ J+ c4 v: n* p1 C% F currentBoardSituation=pBoard; 2 c: V0 Y, M/ C5 K
PlayerMode=min; //当前下子方改为人
0 O6 x; P' p; Y4 Q3 D* G0 L- f Break;
5 B( @3 i% |3 M }
`' G% g r( v' K7 S l) Y# B4 ?} $ n5 g; @+ \/ c$ _! @) [
& q5 Z) {) o6 V9 i 其中对于Search函数的表示如下:实际上核心的算法是一个剪枝过程,其中在这个搜索过程中相关的四个参数为:(1)当前棋局情况;(2)当前的下子方,可以是机器(max)或者是人(min);(3)父节点的值oldValue;(4)当前的搜索深度depth。
% X9 ^5 _( q! q
0 t5 `1 z( S0 E8 V2 ?double Search(CBoardSituation&
- t' S0 c! L2 s1 R, n9 m5 Oboard,int mode,double oldvalue,int depth)
- |$ u* J/ K$ Z0 k$ G{ 8 I/ |2 o" X- m% A: ~
CList m_DeepList;
+ ]" I- Q' [# o1 ^6 m if(deptholdvalue))== TRUE)
- D& L1 M- e: k* x { : }! [7 z% P. v$ I; |( n* }
if(mode==max)
, ?5 I7 `3 Z. H3 ~ P value=select(value,search(successor ; ?' t1 G% i5 _# a, E
Board,min,value,depth+1),max);
9 x0 R$ [8 g/ \: B) J8 X* } else
5 _' a, ]* k- @0 { value=select(value,search(successor 9 N+ O( Z& I! t# r7 S6 G
Board,max,value,depth+1),min);
$ U7 f) o! m1 ?/ {- M- L" U! R }
, e: \* |! A: _. P- e' U1 e8 | return value; - {( Y, I3 ]$ J2 _
}
$ S2 q8 z% ^& x2 A5 ?8 R5 O else
% |! ^5 v2 j& r, Z3 O( P { $ q' v! P# u& Z' X6 K W9 w6 g& I) a
if ( goal(board)<>0) 5 z! Q$ V& [+ u0 \8 D% ]. y/ j
//这里goal(board)<>0表示已经可以分出胜负
# V$ B8 g0 n! W1 Y; h return goal(board); 2 n$ a- p5 k7 T
else 3 c" t: @$ @. L/ n- X# L
return evlation(board);
& _. `" Z a$ I B$ Q- M } ; J+ r9 x% G; n8 u% Z; j1 _6 Z( [
} 5 h- J* H7 q6 d0 q5 N0 Y
4 x( v0 ]6 S6 [7 Z$ Y
注意这里的goal(board)函数是用来判断当前盘面是否可以分出胜负,而evlation(board)是对当前的盘面从机器的角度进行打分。 1 L5 U( Y- @" a$ }% D* f% F* y9 X
2 Z% N+ B1 b% r$ ^+ l 下面是Select函数的介绍,这个函数的主要目的是根据 PlayerMode情况,即是机器还是用户来返回节点的应有的值。 & W. c/ }8 B# q& b3 I4 J$ \
1 {3 }! p# O1 D& @* w, v$ L+ S
double Select(double a,double b,int mode)
5 j* Q; _( a. w! ^, D+ Y{ : }* C' n0 p1 l' ~ N7 c1 t
if(a>b && mode==max)¦¦ (a< b && mode==min)
1 r* V( a2 O& V c. areturn a; 9 h5 _7 K0 [! e! Y
else
- U, Y. c( h5 h( m1 e, n0 zreturn b; " {: C8 a: F1 m- ~' V; y
}
! N5 E R4 d t2 j6 p% @0 [( ^
+ i4 L# o1 O$ Z+ w" Y' X/ d N五、小结
3 z8 v- `' v4 \8 X9 t$ L 在Windows操作系统下,用VC++实现了这个人机对战的五子棋程序。和国内许多只是采用规则或者只是采用简单递归而没有剪枝的那些程序相比,在智力上和时间有效性上都要好于这些程序。同时所讨论的方法和设计过程为用户设计其他的游戏(如象棋和围棋等)提供了一个参考。 |
|