上篇文章发布了源码,这里先将代码做个整体介绍,以方便读者了解整个程序结构。
Board类负责棋盘绘制工作,棋盘是正方形,横竖各19条线,再加上四周边界,所以线与线之间的间隔就是棋盘宽度除以20,
这个宽度也是棋子的直径,另外还要在棋盘上画九个小黑点,标示出星位,代码不再赘述。
再看棋子Stone类,它的主要属性是横纵坐标X、Y,都是[1,19]间的整数,还有手数Number,因为黑棋先下,所以黑棋的手数总是奇数,
而白棋的手数总是偶数,棋子的Color属性就是根据这个规则计算的。另外还有一个重要属性就是KilledStone,用来保存被该棋子吃掉的子,
这主要用在悔棋及复盘时的后退功能,因为当把一个已经下上去的棋子拿掉的话,也要相应的把它吃掉的子再放回棋盘,
另外在判断是否是打劫局面的时候也会用到该属性。Stone类里的主要方法就是Draw()和Die()了,用来绘制和移除棋子。
程序的主要控制类是WeiQi类,只有一个属性runningMode,是运行模式,包括对弈与复盘两种模式。主要职责是负责管理系统资源,
如棋盘、棋子列表、棋谱、行棋手数等,还要响应用户输入,其中最主要的就是鼠标单击事件,也就是用户在棋盘上落子。代码如下:
2 {
3 if (runningMode == RunningMode.Study)
4 {
5 GoNext();
6 return ;
7 }
8
9 Point p = e.GetPosition(_cvsBoard);
10 double gap = _cvsBoard.Width / 20 ;
11 Coordinate cod = Coordinate.GetCoordinateByPoint(p, gap);
12 if ( ! cod.IsValid())
13 {
14 return ;
15 }
16 if (_stoneList.FindStone(cod.X,cod.Y) != null )
17 {
18 return ;
19 }
20
21 Stone stn = new Stone( ++ _stoneNumber, cod.X, cod.Y, _cvsBoard);
22 if (_stoneList.Add(stn))
23 {
24 _stoneRecord.Add(cod);
25 DrawText(stn);
26 }
27 else
28 {
29 _stoneNumber -- ;
30 }
31 }
先判断运行模式,如果是复盘模式,则单击相当于点击下一步按钮。如果是对弈模式,则根据点击位置获得要下子的坐标,
如果该坐标无效或该位置已经有棋子则返回,否则生成新棋子加入棋子列表。如果这步棋是有效行棋,则会添加成功,从而要加入棋谱,
并在该棋子上绘制手数,否则说明是无效行棋,手数再回退一步。至于如何判断行棋有效性,会在下面StoneList里面讲到。
棋谱管理类是StoneRecord,所谓棋谱就是所有有效行棋的坐标列表,按行棋顺序排列,里面提供了前进与后退操作,复盘模式下会用到。
另外还提供了棋谱的打开与保存,采用的是silverlight文件操作功能,代码容易理解,不再赘述。
程序中最重要的类就是StoneList了,用来保存棋盘上当前局面下的棋子,所以关于行棋有效性、吃子、悔棋、数目等算法都是在这儿实现的。
先说行棋有效性。什么样的位置才可以落子呢?当然首先是这个位置不能有子,然而只是满足这个条件还不够。总结起来应该是这样的:
如果一个棋子下在棋盘上是有气的,那么它一定是有效的;否则的话要看它是否能吃掉其它子,如果可以提子,一般来说也是有效的,除了打劫的情况。
从这儿可以看出,要判断行棋是否有效,首先要判断棋盘上的一个棋子是否有气,这也是很多核心算法的基础。棋子是否有气,首先可以看它相邻的位置,
如果这些位置里面有空位,则这个棋子一定有气,否则要看和它相连的同颜色的棋子是否有气,所以算法应该是个递归处理,代码如下:
2 {
3 if (lstLinkedStone == null )
4 {
5 lstLinkedStone = new List < Stone > ();
6 }
7
8 int leftX = stn.X - 1 ;
9 int rightX = stn.X + 1 ;
10 int topY = stn.Y - 1 ;
11 int bottomY = stn.Y + 1 ;
12 Stone leftStone = null ;
13 Stone rightStone = null ;
14 Stone topStone = null ;
15 Stone bottomStone = null ;
16
17 if (leftX > 0 )
18 {
19 leftStone = FindStone(leftX, stn.Y);
20 if (leftStone == null )
21 {
22 return true ;
23 }
24 }
25 if (rightX < 20 )
26 {
27 rightStone = FindStone(rightX, stn.Y);
28 if (rightStone == null )
29 {
30 return true ;
31 }
32 }
33 if (topY > 0 )
34 {
35 topStone = FindStone(stn.X, topY);
36 if (topStone == null )
37 {
38 return true ;
39 }
40 }
41 if (bottomY < 20 )
42 {
43 bottomStone = FindStone(stn.X, bottomY);
44 if (bottomStone == null )
45 {
46 return true ;
47 }
48 }
49
50 if (lstLinkedStone.IndexOf(stn) < 0 )
51 {
52 lstLinkedStone.Add(stn);
53 }
54
55 if (leftX > 0 )
56 {
57 if (leftStone.Color == stn.Color && lstLinkedStone.IndexOf(leftStone) < 0 )
58 {
59 if (IsLive(leftStone, lstLinkedStone))
60 {
61 return true ;
62 }
63 }
64 }
65 if (rightX < 20 )
66 {
67 if (rightStone.Color == stn.Color && lstLinkedStone.IndexOf(rightStone) < 0 )
68 {
69 if (IsLive(rightStone, lstLinkedStone))
70 {
71 return true ;
72 }
73 }
74 }
75 if (topY > 0 )
76 {
77 if (topStone.Color == stn.Color && lstLinkedStone.IndexOf(topStone) < 0 )
78 {
79 if (IsLive(topStone, lstLinkedStone))
80 {
81 return true ;
82 }
83 }
84 }
85 if (bottomY < 20 )
86 {
87 if (bottomStone.Color == stn.Color && lstLinkedStone.IndexOf(bottomStone) < 0 )
88 {
89 if (IsLive(bottomStone, lstLinkedStone))
90 {
91 return true ;
92 }
93 }
94 }
95 return false ;
96 }
代码先判断棋子上下左右四个相邻位置是否为空,如果有空位则返回true,否则在四个方向上递归处理和它同颜色的棋子,只要一旦发现某棋子有气,
则说明该棋子也有气。需要注意的是在递归的过程中,不能重复处理一个棋子,否则会出现死循环,因为如果a和b相连,那么b也和a相连。
所以算法在递归过程中,同时保存了和这个棋子相连的棋子,一方面可以解决这个问题,另外如果一个棋子没气,那所有和它相连的子也没气,
提子时就需要一起提掉,实际上吃子算法就是利用了这一点。有了这个函数,行棋有效性和吃子就容易实现了,代码如下:
2 {
3 if ( ! IsLive(stn, null ))
4 {
5 List < Stone > lstKilledStone = GetKilledStone(stn);
6 if (lstKilledStone.Count < 1 )
7 {
8 return false ;
9 }
10 if (lstKilledStone.Count == 1 )
11 {
12 Stone killedStone = lstKilledStone[ 0 ];
13 if (killedStone.Number == stn.Number - 1 )
14 {
15 if (killedStone.KilledStone.Count == 1 )
16 {
17 return false ;
18 }
19 }
20 }
21 }
22 return true ;
23 }
里面有两种情况会返回false,一是如果该棋子没气,又没有吃掉其它棋子;另外一种就是虽然吃掉了一个棋子,但这个棋子刚好是上一步下的,而且还吃掉了一个子,
显然这就是打劫的情况。至于吃子的算法,也容易理解了,代码如下:
2 {
3 List < Stone > lstDeadStone = new List < Stone > ();
4
5 Stone stn = FindStone(newStone.X - 1 , newStone.Y);
6 if (stn != null && stn.Color != newStone.Color)
7 {
8 CheckAndSaveDeadStone(stn, lstDeadStone);
9 }
10
11 stn = FindStone(newStone.X + 1 , newStone.Y);
12 if (stn != null && stn.Color != newStone.Color)
13 {
14 CheckAndSaveDeadStone(stn, lstDeadStone);
15 }
16
17 stn = FindStone(newStone.X, newStone.Y - 1 );
18 if (stn != null && stn.Color != newStone.Color)
19 {
20 CheckAndSaveDeadStone(stn, lstDeadStone);
21 }
22
23 stn = FindStone(newStone.X, newStone.Y + 1 );
24 if (stn != null && stn.Color != newStone.Color)
25 {
26 CheckAndSaveDeadStone(stn, lstDeadStone);
27 }
28
29 return lstDeadStone;
30 }
31
32 void CheckAndSaveDeadStone(Stone stn,List < Stone > lstDeadStone)
33 {
34 List < Stone > lstLinkedStone = new List < Stone > ();
35 if ( ! IsLive(stn, lstLinkedStone))
36 {
37 foreach (Stone deadStone in lstLinkedStone)
38 {
39 if (lstDeadStone.IndexOf(deadStone) < 0 )
40 {
41 lstDeadStone.Add(deadStone);
42 }
43 }
44 }
45 }
就是在上下左右四个相邻位置上判断相反颜色的棋是否没有气,如果没气,那连同与这个棋子相连接的棋子也全都是死棋。所以在判断棋子是否有气的函数中,
顺便保存与这个棋子相连的棋子,避免了重复计算,是很有效率的处理方式。
最后再说一下数目算法。这里做了简化处理,假定用户已经把残子全部提掉,并收完了全部单官。这样的话,如果一个空点四周全是同颜色的棋,
那这个空点就算作该方的一目。当然这样处理比较简单,毕竟要判断残子的死活还是比较复杂的。代码如下:
2 {
3 int black = 0 ;
4 int white = 0 ;
5
6 for ( int x = 1 ; x < 20 ; x ++ )
7 {
8 for ( int y = 1 ; y < 20 ; y ++ )
9 {
10 Stone stn = FindStone(x, y);
11 if (stn != null )
12 {
13 if (stn.Color == StoneColor.Black) { black ++ ; }
14 else { white ++ ; }
15 }
16 else
17 {
18 int result = IsWhite(x, y);
19 if (result == 0 ) { black ++ ; }
20 else if (result == 1 ) { white ++ ; }
21 }
22 }
23 }
24 return string .Format( " 黑方:{0},白方:{1} " ,black,white);
25 }
26
27 private int IsWhite( int x, int y)
28 {
29 Stone firstStone = null ;
30 Stone tempStone = null ;
31 for ( int left = x - 1 ; left > 0 ; left -- )
32 {
33 tempStone = FindStone(left, y);
34 if (tempStone != null )
35 {
36 firstStone = tempStone;
37 break ;
38 }
39 }
40 for ( int right = x + 1 ; right < 20 ; right ++ )
41 {
42 tempStone = FindStone(right, y);
43 if (tempStone != null )
44 {
45 if (firstStone == null ) { firstStone = tempStone; }
46 else if (firstStone.Color != tempStone.Color) { return - 1 ; }
47 break ;
48 }
49 }
50 for ( int top = y - 1 ; top > 0 ; top -- )
51 {
52 tempStone = FindStone(x, top);
53 if (tempStone != null )
54 {
55 if (firstStone == null ) { firstStone = tempStone; }
56 else if (firstStone.Color != tempStone.Color) { return - 1 ; }
57 break ;
58 }
59 }
60 for ( int bottom = y + 1 ; bottom < 20 ; bottom ++ )
61 {
62 tempStone = FindStone(x, bottom);
63 if (tempStone != null )
64 {
65 if (firstStone == null ) { firstStone = tempStone; }
66 else if (firstStone.Color != tempStone.Color) { return - 1 ; }
67 break ;
68 }
69 }
70 if (firstStone == null ) { return - 1 ; }
71 if (firstStone.Color == StoneColor.Black) { return 0 ; }
72 return 1 ;
73 }