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地址

線上地址

相關文章
相關標籤/搜索