“JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序。这是教程的第1节。
这一节我们设计图形界面,显示初始化棋局。当点击某棋子时,弹窗提示所点击的具体棋子。效果如下:
1.1、棋盘表示
中国象棋有10行9列,很自然地想到可以用10×9矩阵表示棋盘。事实上,我们使用16×16矩阵来表示一个扩充了的虚拟棋盘。
如上图所示,灰色部分为真实棋盘,置于虚拟棋盘之中。这么做可以快速判断棋子是否走出边界。例如象沿田字走,如果走到真实棋盘之外的虚拟棋盘中,说明走法不合法。
容易想到使用二维数组表示16×16矩阵,这样棋盘上的一个位置需要两个变量表示。一个走法包括起点和终点,就需要四个变量。如果使用长度为256的一维数组表示,一个位置只需一个变量,这就可以减少计算量。因此用一维数组表示16×16矩阵。
一维矩阵和二维矩阵之间的转换也很简单:
// 将二维矩阵转换为一维矩阵 function COORD_XY(x, y) { return x + (y << 4); } // 根据一维矩阵,获取二维矩阵行数 function RANK_Y(sq) { return sq >> 4; } // 根据一维矩阵,获取二维矩阵列数 function FILE_X(sq) { return sq & 15; }
其中,sq & 15是通过位运算取余,与sq % 16结果相同(可参考篇文章)。
再使用一个辅助数组,标识虚拟棋盘中,哪些位置属于真实棋盘:
var IN_BOARD_ = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ];
要判断某位置是否在真实棋盘,可使用函数:
function IN_BOARD(sq) { return IN_BOARD_[sq] != 0; }
1.2、棋子表示
使用整数表示棋子:
|
将 |
士 |
象 |
马 |
车 |
炮 |
卒 |
红方 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
黑方 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
棋子这样表示,可以快速判断某棋子属于红方还是黑方,如下表所示:
红方棋子 |
黑方棋子 |
||
十进制 |
二进制 |
十进制 |
二进制 |
8 |
0000 1000 |
16 |
0001 0000 |
9 |
0000 1001 |
17 |
0001 0001 |
10 |
0000 1010 |
18 |
0001 0010 |
11 |
0000 1011 |
19 |
0001 0011 |
12 |
0000 1100 |
20 |
0001 0100 |
13 |
0000 1101 |
21 |
0001 0101 |
14 |
0000 1110 |
22 |
0001 0110 |
可以看出:
红方棋子 & 8 = 1
黑方棋子 & 16 = 1
1.3、字符串表示局面
使用数组表示局面,程序处理起来很方便,但是再网上传递棋局很不方便。我们可以用一行字符串表示一个局面,这就是FEN格式串,一种使用ASCII码字符描述国际象棋局面的标准,当然也可应用于中国象棋。中国象棋的初始局面可表示为:
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1
(1)、红色区域,表示棋盘布局,小写表示黑方,大写表示红方。一个字母表示一个棋子,对应关系如下。
红方 |
字母 |
黑方 |
字母 |
对应单词 |
帅 |
K |
将 |
k |
king |
仕 |
A |
士 |
a |
advisor |
相 |
B |
象 |
b |
bishop |
马 |
N |
马 |
n |
knight |
车 |
R |
车 |
r |
rook |
炮 |
C |
炮 |
c |
cannon |
兵 |
P |
卒 |
p |
pawn |
至于为什么马不用H(horse),象不用E(elephant),这是为了与国际象棋相对应。如果没有棋子,则用数字表示出相邻连续的空位数。中国象棋共有十行,每行都用一个字符串表示,行间使用正斜杠分割。例如:
rnbakabnr表示:
9表示:第二行都是空格。
1c5c1表示:
(2)、绿色区域,表示轮到哪一方走子,“w”表示红方,“b”表示黑方。(没有用r表示红方,我想也是为了与国际象棋对应吧,毕竟国际象棋是黑白两色。)
(3)、深紫色区域,在中国象棋中没有意义,始终用“-”表示。
(4)、紫红色区域,在中国象棋中没有意义,始终用“-”表示。
(5)、蓝色区域,表示双方没有吃子的走棋步数(半回合数),通常该值达到120就要判和(六十回合自然限着),一旦形成局面的上一步是吃子,这里就标记“0”。
(6)、棕色区域,表示当前的回合数。
我们的程序就是使用FEN串初始化棋局的,这就涉及到了将FEN串转化为一维棋局数组。暂时不考虑哪方走子,只解析红色部分,伪代码如下:
// 将FEN串转为一维数组 行变量 y = 3 列变量 x = 3 var c = FEN串第一个字符; while (c != " ") { if (c == "/") { // 换行 x = 3; y ++; if (y > 12) { break; } } else if (c >= "1" && c <= "9") { // 出现空位 列向量x增加c } else if (c >= "A" && c <= "Z") { // 红方棋子 将字符表示的棋子转换为整数,并放入数组x + (y << 4)的位置 } else if (c >= "a" && c <= "z") { 将字符表示的棋子转换为整数,并放入数组x + (y << 4)的位置 } c = FEN串的下一个字符; }
1.4、棋盘前端设计思路
由于棋盘有90个交叉点,我们把棋盘划分为的90个小正方形区域,交叉点是小正方形的中心。每个区域都会定义一个img标签。
上图使用红色方框,标识出了4个小正方形区域。
这些img标签有两个作用:
(1)、显示棋子图片
如果某个区域存在棋子,就会显示相应的棋子图片;否则,显示一张透明图片(也就是oo.gif)。
(2)、响应点击事件
每个img标签都会绑定onmousedown事件。点击不同的img标签时,会传递不同的参数给响应函数,这样就知道点击的具体是哪个区域了。
1.5、核心代码说明
本节的代码可以在 Github 下载,也可以直接clone
git clone -b step-1 https://github.com/Royhoo/write-a-chinesechess-program
程序中定义了两个对象:Board和Position。Board表示一个棋盘,主要功能是初始化棋局,显示棋盘、棋子,响应棋盘上的点击事件。Position存储了一维棋局数组,并定义了很多对该数组进行操作的方法。
Board对象实例化的代码位于index.html中。
通过prototype属性,我们为这两个对象添加了很多的属性和方法。
Board的主要属性和方法:
(1)、pos
这是Position对象的一个实例。
(2)、flushBoard()
刷新棋盘,也就是重新显示棋盘上的棋子。
(3)、drawSquare(sq)
显示sq位置的棋子图片。如果该位置没棋子,则显示一张透明的图片。
(4)、clickSquare(sq_)
点击棋盘的响应函数。点击棋盘(棋子或者空位置),就会调用该函数。sq_是点击的位置。
Position的主要属性和方法:
(1)、squares
这就是一维棋局数组。
(2)、fromFen(fen)
通过FEN串初始化棋局,也就是将参数fen表示的棋局,转化为一维棋局数组squares表示的棋局。
(3)、addPiece(sq, pc)
将棋子pc添加进棋局中的sp位置。