react 版跳棋

react 版跳棋

        最近在学校闲着也是闲着,打算复习一下react,想写点什么东西,最后决定写一个跳棋打发闲暇的时光。最后按照本身设想的写完了,因为是基于create-react-app的架子,不能放在codepen上有一点遗憾,不过本文最后给了线上地址和github地址,你们感兴趣能够看看,欢迎批评指正。css

效果图

chess

整体思路

咱们把跳棋这个项目先拆分为如下步骤前端

  1. 画出棋盘和棋子 (UI 层面)
  2. 判断棋子的可跳路径 (逻辑层面)
  3. 跳棋的动画(UI + 逻辑层面)

关于画出棋盘(UI)

        咱们仔细观察棋盘, 首先棋盘是由6个等边三角形(棋子)和中间一个正六边形(空闲的棋盘)组成。这里就教你们怎么画出这6个等边三角形吧, 先给个示意图吧。react

keyboard

        在画这些棋子以前咱们先作出以下思考,首先这6个三角形是对称的,便可以经过绕某一点旋转获得,其次任意两个棋子的距离是相同的。jquery

第一步: 画出轮廓

         即须要画出 AEI 和 CMG 这两个等边三角形。git

        这一步能够用border实现,这也是比较常规的方法,而后CMG就是AEI旋转180deg获得的图形。这里要注意一下,旋转的中心点是O点,你们要设置好transform-origin.github

        固然最最重要的一点,棋盘是要适配的,即它的宽度不能写死,咱们把它写成一个变量最好了,为了你们看的清楚,我截取一段scss给你们看看。算法

$width: 250px;
$height: $width * sqrt(3);
$rotateY: round(($width * 2 * 2 / 3 ) * sqrt(3) / 2);  
$containerX: 2 * $width;
$containerY: 2 * $rotateY;
$radius: getGap($width, 0.4) / 2;     //0.4 是gap 和 直径的比
$gap: 2 * 0.4 * $radius; 
复制代码

        这里width和rotateY分别指示意图中加粗黑框宽的1/2和,高的1/2。 黑框的宽高分别为上述的containerX,containerY。radius指小球的半径,gap指棋子之间的间距。这里全部的属性只依赖于变量width,方便棋盘的放大和缩小,咱们能够写下以下式子。redux

第二步: 画出棋子(干货来了)

        咱们首先画出角BAN上的10个棋子,咱们从上往下画,一共四层,每一层为当前层数个棋子。咱们把AE上的棋子作为每一层的起始点。数组

width 黑色容器的宽 也为三角形边长 = A E
而三角形的每条边上平均放置了12个棋子,即棋子间距为 width / 12


第一层 chess-0-0  起始点(width/2, 0)
第二层 chess-0-0  起始点(width/2 - 棋子间距/2, 棋子间距 * Math.sqrt(3)/2)
      chess-0-1  (chess-0-0.x + gap, chess-0-0.y)
...

@for $i from 0 to 4{
 	@for $j from 0 to ($i+1){
		left: $width  -  $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3);
		top: $i * $gap + 2 * $radius * $i) * sqrt(3) / 2     
 	}
} 	
复制代码

        这时候棋子单边的棋子就出来了,但是咱们须要6边的棋子呀,难道咱们要一边一边画吗? 答案确定是No NO No啊!浏览器

        好,咱们如今按照咱们以前的思路把角依次BAN旋转60deg。首先咱们有几个注意点:

  • 咱们在绘制棋子的时候left为棋子的左上角,这个左上角并非棋盘的顶点,咱们须要经过css(transform: translate(-50% -50%))将球的左上角的点移至棋盘上。

  • 咱们棋子的父标签是那个黑色的container,而咱们旋转的中心点是上图中的O点。

咱们来推导一些公式 (点的旋转公式)
	A 点坐标 (x1,y1) 与 x 轴夹角为 b
	B 点坐标 (x2, y2) 与 AO 夹角为 c
	这里换算成极坐标
	则 x1 = rcosb      y1 = rsinb      
	   x2 = rcos(b+c) = rcosbcosc - rsinbsinc = x1cosc - y1sinc
	   y2 = rsin(b+c) = rsinbcosc + rcoscsinb = x1sinc + y1cosc
复制代码

        可是咱们的中心点默认是容器的左上角,不是容器的中心点呀。容易,咱们坐标平移一下就行了。

x2 = (x - w)cosc - (y - h)sinc 
y2 = (x - w)sinc	+ (y - h)cisc

这时候的x2,y2 是相对于O中心点旋转后的坐标, 咱们再返到以前的坐标系中。

x2 = (x - w)cosc - (y - h)sinc + w
y2 = (x - w)sinc	+ (y - h)cisc + h
复制代码

        没错,就是这样,咱们如今对BAN旋转吧,贴上scss的代码(话说三层循环真是有一点麻烦呢!)

@for $k from 0 to 6{
	@for $i from 0 to 4{
		@for $j from 0 to ($i+1){
	      .chess-#{$k}-#{$i}-#{$j}{
		      left: cos(60deg * $k) * ($width  -  $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3) - $width) - sin(60deg * $k) * (($i * $gap + 2 * $radius * $i) * sqrt(3) / 2 - $rotateY) + $width;
		      top: sin(60deg * $k) * ($width  -  $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3) - $width) + cos(60deg * $k) * (($i * $gap + 2 * $radius * $i) * sqrt(3) / 2 - $rotateY) + $rotateY; 
	      }
		 }
	}
}
复制代码

最后棋盘就是下面这样了(掘金不支持iframe 你们戳开连接看codepen吧)!!! 是否是颇有趣呢 :)

See the Pen chessBoard by shadowwalkerzero ( @shadowwalkerzero) on CodePen.

第三步 画出棋盘

        咱们如今须要画出棋盘上的点,即棋子能够放的点。拆分一下棋盘,棋盘是由中心的正六边形和那6个角组成,正6边形按照咱们以前的方法绘制是否是很简单呢? 就是把三角形上的点绘出来,而后旋转6次就行了。这里就不赘述了。

计算棋子可跳路径(逻辑)

由于棋子都是绝对定位的,咱们要计算下一跳的点,必然要计算出它的精确坐标呀。但是我该怎么表示这些点呢?拿二维坐标吗?固然能够了,毕竟是2d,可是这样就太笨了,太笨了!

咱们须要观察一下棋盘,其实棋子能够跳的点最终能够表现为6边形,画个示意图吧。

img

因此咱们须要把跳棋上的点表示成3元组。例如正六边形斜上方的点就该表示成chess-1-2-2 单位是当前轴上两个点的距离。

这里干脆也把给棋子编号的方法也告诉你们吧。其实也很简单,就是利用点到直线间距离公式( d = Math.abs(AX + BY + C) / Math.sqrt(A^2+B^2); )

咱们对一个点分别向3条轴计算三次距离,距离同样的就在一条线上。

看一下编号结束后的棋盘吧。

img

计算棋子的落点(广度优先)

这里咱们须要明确一下跳棋的规则,跳棋是既能够向周围滚一步,也能够隔着棋子跳的。 为了标示棋盘该点已被占用,咱们须要引入一个属性isOccupy来标示。这里给出棋盘上的点的数据结构。

{
        key: `,
        isChess: ,
        locate: '',
        style: {
          background: ,
          left: ,
          top:,
          zIndex: 2,
          transform: 
}
复制代码

这里解释一下各个属性 isChess 用来区分棋盘上的点和棋子,locate表示棋子或棋盘上的点的编号。style标示棋子或棋盘上的点坐标,还有一些辅助属性,好比当前要走的棋子会显得大一点。既然咱们已经获取到了关于棋子和棋盘上的全部信息,下一步就是要让棋子跳起来了。

咱们再画一个简单的示意图
	
	 X 0 (0) X 0 
咱们以 0 表示棋子, X表示棋盘上的空点。(0) 表示正要跳的棋子。

显然流程异常的简单:
1. 从当前(0) 位置分别向左,右搜寻,直至找到左边和右边的距离最近0(注意咱们是三条轴,分别向三条轴搜寻)。
2. 以刚找到的点为基点,当正要跳的棋子和找到的点距离为长度,找出对称的点,即棋子的	落点。
3. 将上一步的落点作为当前点。
回到第一步
复制代码

稍微分析一下 会发现是很简单的递归,发现从当前点向左右搜寻找点,真是和二叉树如出一辙,问题就转变为二叉树的遍历上了。

当让遍历方法很是多,深度优先算法和广度优先算法均可以,可是做者这里推荐广度优先算法,由于广度优先算法调试更方便,层数浅,我也是基于广度优先算法实现的。

咱们这里简单缕一下广度优先算法的思路,写一下伪码。

思路是 一个队列 path: []
压入左右搜寻的点 path.push[A.left, A.right]
压出left  path: [A.right]
压入A.left 左右搜寻的点 path: [A.right, A.left.left, A.left.right]


//itemMove 指当前要跳的棋子
//position 放置了棋盘上的点和棋子
// 广度优先队列
//passNode 收集棋子的落点

calculatePath = (itemMove, position, allPath, passNode) => {
	let path = getValidPoint(itemMove) //获取三条轴上的落点
	allPath.push(...path);
	
	if(allPath.length > 1){
	   let  nextJump = allPath[0];
	   allPath.splice(0, 1);
	   passNode.push(nextJump);	//这就是下一跳了
	}
	 return nextJump ? calculatePath(nextJump, position, allPath, passNode) : passNode;
}
复制代码

固然这里有一个小问题,即成环的问题,你跳过去,下一跳又给你跳回来,就会死循环。这个问题解决的方法也不少,把走过的路径节点都标示一下,参照上面的伪码,全部的路径节点都在pressNode下,只要这个节点走过了,就不容许再走一遍。

如今要让棋子真的跳起来(深度优先)

为了更好的交互,让跳棋跳起来是必须的!咱们先捋捋咱们现有的数据

  • 跳棋的起跳点和最终的落点,
  • 以及中间的过渡点(就是上一节中跳棋的全部落点)。

而后咱们的问题: 就是当用户点击任意一个落点时,要让跳棋一级一级的跳过去

为了把路径肯定出来,咱们必须把这些过渡点链接起来,当用户点击任意一个落点时候,咱们须要计算从起跳点和落点的距离。

1.来把这些落点链接起来吧

在肯定跳棋的落点的时候,咱们检索出了一个棋子的全部落点。为了避免让这些数据丢失, 咱们能够用记录一下。

nextJump.parent = startJump
复制代码

nextJump 就是startJump 的全部落点,咱们用parent来保存它们的联系。如今咱们就要把它们串起来了,先从简单的例子出发吧。在设置为parent后,咱们大概获得了一组相似这样的数据。

let points = [{
    name: 'A',
    parents: ['C']
},{
    name: 'C',
    parents: ['D']
}, {
    name: 'D',
    parents: ['E']
}, {
    name: 'E',
    parents: ['L', 'F']
}, {
    name: 'F',
    parents: ['C']
}, 
{
    name: 'L',
    parents: []
}]
复制代码

上面的数据对应的示意图以下,大体为一个联通图。

access

假设咱们从起点A出发要到终点L,求出A - L 的路径。常规方法就是深度优先了。咱们简单描述一下流程(主要注意成环的问题)。

1.路径队列 [L] 当前节点 L
2.得到 L 的 parent [E]
3.E进栈 [L,E]
4.E. 作为下一个节点,要是 E 没有 parent 或者成环 E 出栈
重复1 直至找到A

附上代码
let flag = false;
function scanPath(start, end, path) {
 let nextLists = getParents(start);	//获取节点的parent
 let nextJump = false;

for (let i = 0; i < nextLists.length; i++) {
    nextJump = nextLists[i];
    if (path.indexOf(nextJump) < 0) {
        !flag && path.push(nextJump);
        if (nextJump === end) {
            flag = true;
        }
        !flag && scanPath(nextJump, end, path);
    }
}
	!flag && path.pop();
	return path;
}
复制代码

这里咱们就把起始点和落点的路径找出来了,如今就要让棋子作动画了。

2.棋子跳吧(做者没有很好的解决)

咱们描述一下咱们上一步得到的路径,大体为 ['11-2-4', '6-8-13', '14-8-9', '9-3-8']。这里的元素对应上述咱们对棋盘编号的三元组。 表示 棋子要从 11-2-4 -> 6-8-13 -> 14-8-9 -> 9-3-8 一路跳过去。

彷佛实现也不难,在咱们刚学前端的时候,不借助react也能够作到,对dom作tiansition动画,而后监听onTransitionEnd事件,在这里面继续作下一步动画,本身也试着用这种最土的方法作。只是在react中一切都是state了。

好比当前节点要跳 4跳,咱们拿到路径数组 ['11-2-4', '6-8-13', '14-8-9', '9-3-8'] 起跳点 11-2-4 咱们 找到11-2-4的棋子 把它的style 设置成 数组的[index] 就行了,我这里的解决方案是。

styles.map((item, index) => {
      setTimeout(() => {
        this.setState({
          nowStyle: styles[index]
        })
      }, 600 * (index + 1));
    });
复制代码

styles 就是路径数组里路径节点的style,主要是left,top。nowStyle 就是起跳的棋子要不断应用的style。放一张本身的测试图,时间为600ms的缘由是由于transition的时间是 500ms,总要先让动画作完把。

img

可是这里我并不认为这个方案可行,react的diff render时间 还有不一样浏览器性能的时间都不可控,settimeout真是下下策。

中间也求助过一些很优秀的react动画库,好比react-motion。发现它能作一组动画的只有StaggeredMotion,可是在文档中,做者写明了:

(No onRest for StaggeredMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.)

就是对组动画不提供回调,也就是说咱们无法监听这组动画里的某一个动画,真是遗憾。

因为做者并不以为这个解决方案很好,因此没有放在应用在项目的线上中,可是放在github目录下,感兴趣的同窗能够提供本身的解决方案。

一些零散的问题

  1. 好比怎么判断输赢

    这个问题咱们能够在初始化棋盘就解决掉,好比假设如今执棋方是绿色,那么它的目的地是粉色,一开始的时候就把各个执棋方的目的地的位置计算好,每走一步,就check一下。

  2. 好比怎么作到棋手轮流下

    这个咱们须要一个状态位控制,表示当前棋手,下完一步,加1对全部选手取余就行了。

关于react动画的一点思考

如下为本人我的观点,不保证正确。

  1. react作这种须要必定计算的网页,最让我担忧的是性能,每走一步就涉及到多个状态,好比isOccupy 占位,下一跳的坐标。要是setstate({}) 确定不行,由于这是异步的,会批量处理。因此只能setstate((prevState, prevProps) => {}),这样大量的diff,对性能确定是个挑战。这里做者是没有实时更新数据的,计算完一次更新,可是这样就不方便state 调试,并且redux写多了,数据一旦不更新,内心就很慌。

  2. react 因为数据驱动,确实代码更加简洁,可是相比以前写的原生动画,状态太多,全部的状态都挤在state里,逻辑会很的很混乱(也有多是本身水平有限)

  3. 我以为react并不适应动画场景,咱们知道jquery 的animate自己也是基于setInterval实现的,而react 自己框架极其复杂,咱们很难把控时间(也是本身水平有限)。

项目相关

github地址

线上地址

相关文章
相关标签/搜索