其他

JS 中国象棋程序(0):界面设计

2017-03-06 伯乐专栏/未济 前端大全

(点击上方公众号,可快速关注)


作者:伯乐在线专栏作者 - 未济

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


“JavaScript中国象棋程序” 这一系列教程将带你从头使用JavaScript编写一个中国象棋程序。希望通过这个系列,我们对博弈程序的算法有一定的了解。同时,我们也将构建出一个不错的中国象棋程序。


程序的最终效果点击这里查看(http://www.royhoo.cn/chess/web/index.php?r=site/chess)。


在进入正题之前,本节是一些闲聊。如果你急切想进入正题,请跳过本节。


我学习中国象棋程序的历程


最初是买了本书《C/C++中国象棋程序入门与提高》。这是本好书,写得细致、透彻,我很愉快地读完了前6章,毫无压力。这6章讲解了局面表示、走法生成、局面评估、基本搜索算法等内容。遗憾的是,第7章我读不下去了。第7章是在讲解,如何使用VC6.0设计图形用户界面。天啊,我是一个web程序员,我只想了解一下象棋程序的设计思想以及算法,我实在不想去使用陈旧的VC6.0来学习windows GUI编程啊。


幸运的是,我在这本书最后一页的参考文献里,发现了象棋百科全书网。在这家网站的github仓库,发现了一个JavaScript版本的中国象棋软件,而且性能还不错。使用HTML + JavaScript来设计界面,自然是简单了很多啊,这样就能集中精力去学习象棋程序的算法了。


为什么选择JavaScript


本教程之所以选择JavaScript,讲解JavaScript版本的中国象棋程序,我有以下几个理由吧:


1、这个JavaScript版本的中国象棋程序,性能还不错。

2、界面设计简单,可以把主要精力用在对算法的学习上。

3、不用搭建环境。只要有文本编辑器(比如notepad++)和浏览器(最好是chrome吧),就足够了。

4、程序用到的都是很基础的JavaScript语法,应该没有语法方面的障碍。


项目初衷


曾经读到过一个教程手把手教你构建 C 语言编译器,我觉得很有意思。于是我就仿照这它的格式,写下了这个中国象棋程序教程。本教程的绝大部分思想都来自《C/C++中国象棋程序入门与提高》和象棋百科全书网。象棋百科全书网上面还有很多不错的文章,让我受益匪浅。

如果你想了解原版的程序,请前往象棋百科全书网的github下载,这里面有很多个版本,我们使用的是JavaScript版。


这个教程难学吗?


1、至少前4节是不难的吧,都是一些基本的东西。

2、第5节介绍了Alpha-Beta搜素,这个算法很重要,是后面几节教程的基础。

3、如果搞明白了Alpha-Beta搜索算法,随后的3节应该也不算难吧,都是在Alpha-Beta算法的基础上进行优化。

最后,非常感谢象棋百科全书网的前辈以及《C/C++中国象棋程序入门与提高》的作者。

祝你学得愉快。

这一节我们设计图形界面,显示初始化棋局。当点击某棋子时,弹窗提示所点击的具体棋子。效果如下:



 

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 = 1


黑方棋子 & 16 = 1


1.3、字符串表示局面


使用数组表示局面,程序处理起来很方便,但是再网上传递棋局很不方便。我们可以用一行字符串表示一个局面,这就是FEN格式串,一种使用ASCII码字符描述国际象棋局面的标准,当然也可应用于中国象棋。中国象棋的初始局面可表示为:


rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w – – 0 1


(1)、红色区域,表示棋盘布局,小写表示黑方,大写表示红方。一个字母表示一个棋子,对应关系如下。



至于为什么马不用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(https://github.com/Royhoo/write-a-chinesechess-program/tree/step-1) 下载,也可以直接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位置。



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能


专栏作者简介 ( 点击 → 加入专栏作者 


未济:关注C,PHP,MySQL,JAVA,数学,美剧(尤其是权利的游戏)。目前主要关注nlp。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存