前端谈谈实现五子棋

简介: 秉承着会就分享,不会就折腾的技术宗旨。自己利用周末的时间将休闲小游戏-五子棋重新梳理了一下,整理成一个小的教程,分享出来给大家指点指点。

秉承着会就分享,不会就折腾的技术宗旨。自己利用周末的时间将休闲小游戏-五子棋重新梳理了一下,整理成一个小的教程,分享出来给大家指点指点。


五子棋规则


五子棋的规则我简单梳理并且改造如下哈:


  1. 对局双方各执一色棋子;
  2. 空棋盘开局;
  3. 黑先、白后或者白先、黑后,交替下子,每次只能下一子;
  4. 横线、竖线或者斜线上有连续五个同一色的棋子,则游戏结束;


正式比赛的规则,可以戳百度百科了解下哈--五子棋


代码骨架


这里实现的五子棋小游戏是使用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棋盘的物理和逻辑方式。


image.png


绘制物理棋盘,我们这里使用到了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的知识点来绘制棋子。


image.png


// 绘制黑棋或白棋
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();
}


落子实现人人对战


上一节的绘制黑棋和白棋的方法是在单独一个页面出来绘制的。现在我们将绘制棋子和棋盘整合,并实现人人对战的下棋模式。


image.png


我们要监听点击在棋盘上的事件,然后关联物理棋盘和逻辑棋盘点,之后在相应的地方刻画棋子即可。


// 监听落子
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;
    }
  }
}


实现悔棋


在双方下棋的时候,允许双方对已经下的棋子进行调整,也就是悔棋。如下截图展示功能:


image.png


实现悔棋功能的时候,需要知道下棋的历史记录和当前的落子步数和角色。对于历史的记录,这里对每一步的落子都使用一个对象进行存储,并放到一个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; 
  }
}


image.png


胜利提示/游戏结束


五子棋的的结束也就是必须要决出胜利者,或者是棋盘没有位置可以下棋了。这里考虑决出胜利为游戏结束的切入点,上面也说到了如何才算是一方获胜--横线、竖线或者斜线上有连续五个同一色的棋子。那么我们就对这四种情况进行处理,我们在矩阵中记录当前点击的数组点中是否有连续的五个1(黑子)或者连续的五个2(白子)即可。如下截图的x轴上的白子获胜情况,注意gif图右侧打印出来的数组内容:


image.png


// 裁判观察棋子,判断获胜一方
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上愉快玩耍的休闲小游戏-五子棋了。


相关文章
|
JavaScript 前端开发
React+html2canvas+jspdf+antd快速实现前端pdf预览及打印
文章的总结目标实际上就是一个前端pdf打印组件,由於能在往后的其他项目中得以快速上手,并能根据所在项目需要快速自定义扩展,因此組件非常简陋直白,文章是实践过程的记录产物,并不保证完全正确,仅作参考。
React+html2canvas+jspdf+antd快速实现前端pdf预览及打印
|
前端开发 JavaScript
微前端实现方案之iframe
微前端实现方案之iframe
微前端实现方案之iframe
|
JavaScript 算法 前端开发
【前端算法】JS实现数字千分位格式化
JS实现数字千分位格式化的几种思路,以及它们之间的性能比较
541 1
|
JavaScript 前端开发 IDE
如何从一台新电脑配置前端常用工具实现快速开发
如何从一台新电脑配置前端常用工具实现快速开发
如何从一台新电脑配置前端常用工具实现快速开发
|
前端开发
前端实现导出word(docxtemplater、pizzip、jszip-utils、file-saver )
docxtemplater、pizzip、jszip-utils、file-saver 前端实现导出word
1765 0
前端实现导出word(docxtemplater、pizzip、jszip-utils、file-saver )
|
存储 JSON JavaScript
|
JavaScript 前端开发
vue 前端实现随机背景色
vue 前端实现随机背景色
vue 前端实现随机背景色
|
存储 前端开发 算法
一行代码解决LeetCode实现 strStr()使用JavaScript解题|前端学算法
一行代码解决LeetCode实现 strStr()使用JavaScript解题|前端学算法
303 0
一行代码解决LeetCode实现 strStr()使用JavaScript解题|前端学算法
|
移动开发 自然语言处理 小程序
前端不使用 i18n,如何优雅的实现多语言?
前端不使用 i18n,如何优雅的实现多语言?
前端不使用 i18n,如何优雅的实现多语言?
|
存储 移动开发 前端开发
Vue-Router 前端路由实现的思路
Vue-Router 前端路由实现的思路
263 0
Vue-Router 前端路由实现的思路

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
  • 2
    前端工程化演进之路:从手工作坊到AI驱动的智能化开发
  • 3
    Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
  • 6
    实现“永久登录”:针对蜻蜓Q系统的用户体验优化方案(前端uni-app+后端Laravel详解)-优雅草卓伊凡
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距