秉承着会就分享, 不会就折腾的技术宗旨. 自己利用周末的时间将休闲小游戏 - 五子棋重新梳理了一下, 整理成一个小的教程, 分享出来给大家指点指点.
五子棋规则
五子棋的规则我简单梳理并且改造如下哈:
对局双方各执一色棋子;
空棋盘开局;
黑先, 白后或者白先, 黑后, 交替下子, 每次只能下一子;
横线, 竖线或者斜线上有连续五个同一色的棋子, 则游戏结束;
正式比赛的规则, 可以戳百度百科了解下哈 -- 五子棋.
代码骨架
这里实现的五子棋小游戏是使用 JavaScript 语言进行编写的, 使用到了 es6 语法, 面向对象的思想进行.
- // 设置五子棋类
- class Gobang {
- constructor(options={}){
- this.options = options;
- this.init();
- }
- init() {
- const { options } = this;
- }
- }
- // 实例化对象
- let gobang = new Gobang({});
上面的 Gobang 类中, 包含了一个 constructor 和 init 方法. 其中 constructor 方法是类默认的方法, 通过 new 命令生成对象实例时候, 自动调用该方法. 一个类必须有一个 constructor 方法, 如果没有显式定义, 一个空的 constructor 方法会默认添加. 然后就是 init 方法了, 这里我是整个类的初始化的入口方法. 使用类进行的面向对象方法进行编写, 比较好管理代码和功能的扩展.
绘制棋盘
棋盘分为两种, 一种是视觉 (物理) 上的棋盘, 另外一个是逻辑上的棋盘, 你是看不见的. 下面的一张图就很形象地展示了 20*20 棋盘的物理和逻辑方式.
绘制物理棋盘, 我们这里使用到了 canvas 的相关知识点, 控制画笔绘制棋盘:
- // 绘制出物理棋盘
- drawChessBoard() {
- const context = this.chessboard.getContext('2d');
- const {padding, count, borderColor} = this.options.gobangStyle;
- let half_padding = padding/2;
- this.chessboard.width = this.chessboard.height = padding * count;
- context.strokeStyle = borderColor;
- // 画棋盘
- for(var i = 0; i <count; i++){
- context.moveTo(half_padding+i*padding, half_padding);
- context.lineTo(half_padding+i*padding, padding*count-half_padding);
- context.stroke(); // 这里绘制出的是竖轴
- context.moveTo(half_padding, half_padding+i*padding);
- context.lineTo(count*padding-half_padding, half_padding+i*padding);
- context.stroke(); // 这里绘制出的是横轴
- }
- }
这里使用到的 padding,count,borderColor 等都是在实例化的时候传进去的. 这样提高了可配置性和管理. 上面的代码是绘制物理上的棋盘, 那么逻辑上的棋盘虽然不能够绘制出来, 但是我们可以表示出来. 这里我们使用了二维数组的方法去记录逻辑位置, 比如 (0,0) 点对应的数组下标是 [0][0]; 然后(1,2) 点对应的下标是[1][2]... 以此类推. 然后我们再为这个逻辑点赋值为 0, 表示当前点没有落子.
- // 绘制逻辑矩阵棋盘
- initChessboardMatrix(){
- const {count} = this.options.gobangStyle;
- const checkerboard = [];
- for(let x = 0; x < count; x++){
- checkerboard[x] = [];
- for(let y = 0; y < count; y++){
- checkerboard[x][y] = 0;
- }
- }
- }
物理棋盘和逻辑棋盘有了之后, 就可以考虑到将物理棋盘和逻辑棋盘关联起来了. 这个比较简单, 就是要计算真实的单元格位置进行除法操作即可. 这步的管理在后面的落子步骤有提到.
绘制棋子
五子棋的棋子有且仅有两种 -- 黑色棋子或者白色棋子. 这里也是使用 canvas 的知识点来绘制棋子.
- // 绘制黑棋或白棋
- drawChessman(x , y, isBlack){
- const context = this.chessboard.getContext('2d');
- let gradient = context.createRadialGradient(x, y, 10, x-5, y-5, 0);
- context.beginPath();
- context.arc(x, y, 10, 0, 2 * Math.PI);
- context.closePath();
- if(isBlack){
- gradient.addColorStop(0,'#0a0a0a');
- gradient.addColorStop(1,'#636766');
- }else{
- gradient.addColorStop(0,'#d1d1d1');
- gradient.addColorStop(1,'#f9f9f9');
- }
- context.fillStyle = gradient;
- context.fill();
- }
落子实现人人对战
上一节的绘制黑棋和白棋的方法是在单独一个页面出来绘制的. 现在我们将绘制棋子和棋盘整合, 并实现人人对战的下棋模式.
我们要监听点击在棋盘上的事件, 然后关联物理棋盘和逻辑棋盘点, 之后在相应的地方刻画棋子即可.
- // 监听落子
- listenDownChessman() {
- // 监听点击棋盘对象事件
- this.chessboard.onclick = event => {
- let {padding} = this.options.gobangStyle;
- let {
- offsetX: x,
- offsetY: y,
- } = event;
- x = Math.abs(Math.round((x-padding/2)/this.lattice.width));
- y = Math.abs(Math.round((y-padding/2)/this.lattice.height));
- if(this.checkerboard[x][y] !== undefined && Object.is(this.checkerboard[x][y],0)){
- this.checkerboard[x][y] = this.role;
- // 这里调用刻画棋子的方法
- this.drawChessman(x,y,Object.is(this.role , 1));
- // 切换棋子的角色
- this.role = Object.is(this.role , 1) ? 2 : 1;
- }
- }
- }
实现悔棋
在双方下棋的时候, 允许双方对已经下的棋子进行调整, 也就是悔棋. 如下截图展示功能:
实现悔棋功能的时候, 需要知道下棋的历史记录和当前的落子步数和角色. 对于历史的记录, 这里对每一步的落子都使用一个对象进行存储, 并放到一个 history 的数组里面进行保存.
- // 悔棋
- regretChess() {
- if(this.history.length){
- const prev = this.history[this.currentStep - 1];
- if(prev){
- const {
- x,
- y,
- role
- } = prev;
- this.minusStep(x,y);
- this.checkerboard[prev.x][prev.y] = 0;
- this.currentStep--;
- this.role = Object.is(role,1) ? 1 : 2;
- }
- }
- }
- // 销毁棋子
- minusStep(x, y) {
- const context = this.chessboard.getContext('2d');
- const {padding, count} = this.options.gobangStyle;
- context.clearRect(x*padding, y*padding, padding,padding);
- }
上面的代码确实是实现了悔棋功能, 但是, 在实现悔棋的时候, 已经破坏掉了棋盘的 UI, 因为我们是使用 canvas 的 clearRect 方法, 将撤销的棋子使用新的四边形进行覆盖, 那也就覆盖了撤销棋子处的物理棋盘了. 为了弥补这个被覆盖的物理棋盘, 我们得重新绘制出此处坐标的新物理棋盘线条. 这里的修复要考虑到落子在棋盘的不同位置, 要分九种不同的情况进行修复:
左上角棋盘
左边缘棋盘
左下角棋盘
下边缘棋盘
右下角棋盘
右边缘棋盘
右上角棋盘
上边缘棋盘
中间 (非边界) 棋盘
- // 修补删除后的棋盘, 将九种情况的不同参数传过来即可
- fixchessboard (a , b, c , d , e , f , g , h){
- const context = this.chessboard.getContext('2d');
- const {borderColor, lineWidth} = this.options.gobangStyle;
- context.strokeStyle = borderColor;
- context.lineWidth = lineWidth;
- context.beginPath();
- context.moveTo(a , b);
- context.lineTo(c , d);
- context.moveTo(e, f);
- context.lineTo(g , h);
- context.stroke();
- }
实现撤销悔棋
有允许悔棋, 那么就有允许撤销悔棋这样子才合理. 同悔棋功能, 撤销悔棋是需要知道下棋的历史记录和当前的步骤和棋子角色的.
- // 撤销悔棋
- revokedRegretChess(){
- const next = this.history[this.currentStep];
- if(next) {
- this.drawChessman(next.x, next.y, next.role === 1);
- this.checkerboard[next.x][next.y] = next.role;
- this.currentStep++;
- this.role = Object.is(this.role, 1) ? 2 : 1;
- }
- }
胜利提示 / 游戏结束
五子棋的的结束也就是必须要决出胜利者, 或者是棋盘没有位置可以下棋了. 这里考虑决出胜利为游戏结束的切入点, 上面也说到了如何才算是一方获胜 -- 横线, 竖线或者斜线上有连续五个同一色的棋子. 那么我们就对这四种情况进行处理, 我们在矩阵中记录当前点击的数组点中是否有连续的五个 1(黑子)或者连续的五个 2(白子)即可. 如下截图的 x 轴上的白子获胜情况, 注意 gif 图右侧打印出来的数组内容:
- // 裁判观察棋子, 判断获胜一方
- checkReferee(x , y , role) {
- if((x == undefined)||(y == undefined)||(role==undefined)) return;
- const XContinuous = this.checkerboard.map(x => x[y]); // x 轴上连杀
- const YContinuous = this.checkerboard[x]; // y 轴上连杀
- const S1Continuous = []; // 存储左斜线连杀
- const S2Continuous = []; // 存储右斜线连杀
- this.checkerboard.forEach((_y,i) => {
- // 左斜线
- const S1Item = _y[y - (x - i)];
- if(S1Item !== undefined){
- S1Continuous.push(S1Item);
- }
- // 右斜线
- const S2Item = _y[y + (x - i)];
- if(S2Item !== undefined) {
- S2Continuous.push(S2Item);
- }
- });
- }
至此, 已经一步步讲解完如何开发一个能够在 pc 上愉快玩耍的休闲小游戏 - 五子棋了. 不妥之处还请指正哈 @~@
后话
五子棋的体验地址 -- 休闲游戏 - 五子棋 http://reng99.cc/demos/src/gobang/
文章首发地址 --GitHub - 五子棋游戏 https://github.com/reng99/blogs/issues/3
代码仓库地址 --GitHub - 五子棋教程
创作文章不易, 既然都看到这里了, 留个赞再走呗~
来源: https://juejin.im/post/5c18fc9fe51d4570e5715ee2