上篇文章发布了源码,这里先将代码做个整体介绍,以方便读者了解整个程序结构。
Board类负责棋盘绘制工作,棋盘是正方形,横竖各19条线,再加上四周边界,所以线与线之间的间隔就是棋盘宽度除以20,
这个宽度也是棋子的直径,另外还要在棋盘上画九个小黑点,标示出星位,代码不再赘述。
再看棋子Stone类,它的主要属性是横纵坐标X、Y,都是[1,19]间的整数,还有手数Number,因为黑棋先下,所以黑棋的手数总是奇数,
而白棋的手数总是偶数,棋子的Color属性就是根据这个规则计算的。另外还有一个重要属性就是KilledStone,用来保存被该棋子吃掉的子,
这主要用在悔棋及复盘时的后退功能,因为当把一个已经下上去的棋子拿掉的话,也要相应的把它吃掉的子再放回棋盘,
另外在判断是否是打劫局面的时候也会用到该属性。Stone类里的主要方法就是Draw()和Die()了,用来绘制和移除棋子。
程序的主要控制类是WeiQi类,只有一个属性runningMode,是运行模式,包括对弈与复盘两种模式。主要职责是负责管理系统资源,
如棋盘、棋子列表、棋谱、行棋手数等,还要响应用户输入,其中最主要的就是鼠标单击事件,也就是用户在棋盘上落子。代码如下:
鼠标单击
1
void
CvsBoard_MouseLeftButtonDown(
object
sender, MouseButtonEventArgs e)
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了,用来保存棋盘上当前局面下的棋子,所以关于行棋有效性、吃子、悔棋、数目等算法都是在这儿实现的。
先说行棋有效性。什么样的位置才可以落子呢?当然首先是这个位置不能有子,然而只是满足这个条件还不够。总结起来应该是这样的:
如果一个棋子下在棋盘上是有气的,那么它一定是有效的;否则的话要看它是否能吃掉其它子,如果可以提子,一般来说也是有效的,除了打劫的情况。
从这儿可以看出,要判断行棋是否有效,首先要判断棋盘上的一个棋子是否有气,这也是很多核心算法的基础。棋子是否有气,首先可以看它相邻的位置,
如果这些位置里面有空位,则这个棋子一定有气,否则要看和它相连的同颜色的棋子是否有气,所以算法应该是个递归处理,代码如下:
判断棋子是否有气
1
bool
IsLive(Stone stn,List
<
Stone
>
lstLinkedStone)
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相连。
所以算法在递归过程中,同时保存了和这个棋子相连的棋子,一方面可以解决这个问题,另外如果一个棋子没气,那所有和它相连的子也没气,
提子时就需要一起提掉,实际上吃子算法就是利用了这一点。有了这个函数,行棋有效性和吃子就容易实现了,代码如下:
行棋有效性判断
1
public
bool
IsValid(Stone stn)
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,一是如果该棋子没气,又没有吃掉其它棋子;另外一种就是虽然吃掉了一个棋子,但这个棋子刚好是上一步下的,而且还吃掉了一个子,
显然这就是打劫的情况。至于吃子的算法,也容易理解了,代码如下:
计算被新下的棋子吃掉的棋
1
List
<
Stone
>
GetKilledStone(Stone newStone)
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
}
就是在上下左右四个相邻位置上判断相反颜色的棋是否没有气,如果没气,那连同与这个棋子相连接的棋子也全都是死棋。所以在判断棋子是否有气的函数中,
顺便保存与这个棋子相连的棋子,避免了重复计算,是很有效率的处理方式。
最后再说一下数目算法。这里做了简化处理,假定用户已经把残子全部提掉,并收完了全部单官。这样的话,如果一个空点四周全是同颜色的棋,
那这个空点就算作该方的一目。当然这样处理比较简单,毕竟要判断残子的死活还是比较复杂的。代码如下:
数目算法
1
public
string
Calculate()
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
}