用React寫一個數字華容道,你需要知道的祕密

地鐵上的小前端發表於2018-02-13

還在上班?很無聊?數字華容道暢玩地址

開發原始碼地址

這個叫前言

年末了。哦,不,要過年了。以前只能一路站到公司的我,今早居然是坐著過來的。新的一年,總要學一個新東西來迎接新的未來吧,所以選擇了一直未碰的那個據說是全宇宙最牛逼的前端框架-React,在上下班的地鐵上看了兩天官方教程,so what。光看不練假把式,於是就想著做個什麼,偶然看到一個妹妹發了一條關於玩數字華容道,根本停不下來的朋友圈,這遊戲我在今年的最強大腦看過,但是就看兩小天才在滑呀滑呀滑,感覺還不錯,程式猿就該多玩益智類,少玩什麼跳一跳。於是去商店下了個,玩著還行,就是廣告太多,而且只能玩五階以下的,看起不難,一個想法湧於腦上,何不拿這練練手做個Demo,畢竟我們屬於智慧家族的。閒話扯完,進入正題。本文包含但不僅包括以下內容:

  • Demo 開發環境
  • 數字華容道里的祕密
  • 講一講裡面演算法的實現
  • React的使用感受及易錯點

Demo 開發環境

React:16.2.0

react-router-dom:4.2.2

webpack:3.8.1

JS:ES6

CSS:Scss

React教程

專案目錄結構(很適合React入門 如果感覺不錯,請留下你的star):如下圖所示

專案目錄結構

數字華容道里的祕密

玩法說明

數字華容道里在外國被稱為puzzle,譯為數字推盤,最經典的就是puzzle15的高價懸賞13 15 14局面的解。怎麼玩,一圖勝千言,簡單來說就是講左圖的亂選狀態,滑成右圖所示的順序狀態。個人感覺和小時候玩的推箱子有點類似。

clipboard.png

你不知道的祕密

其實寫完Demo的隨機序列頁面生成和操作互動就用了一個周天和一個週一晚上,但到現線上上這個樣子,又多用了三四個晚上(白天上班,晚上學習,這就是前端的日常),為什麼?因為當時基礎版部署到線上,讓女朋友試一把,結果玩個三階的,都兩分鐘了還在折騰(我最快是18秒還原),怎麼這麼笨,於是搶過來,搗騰,再搗騰,怎麼回事,感覺無解啊,於是去百度了一下,在知乎裡看到了這個問題,還真的有無解的情況,問題地址。看圖,後面還要講(敲黑板)

clipboard.png

噢,原來是醬紫。光隨機生成一個亂序數列是不夠的,還得保證這個數列的逆序數為偶數,嗦嘎。於是在隨機序列的生成中又多加一個過程,判斷序列逆序數奇偶性,並調整。早上地鐵上自己又不斷玩玩測試,三階ok,四階ok,五階ok,然後又一遍,一遍。原以為這樣就大功告成了,但是出現了這樣的畫面。有圖。誰剛說的四階ok。。。。於是又測試了多次,發現三階,五階確實ok,但四階確實有Bug。Why,後面自己每開一局,截一張圖,無解的,標記下來,下面就是幾張立功的圖片。

立了大功的幾張圖
下午年會,領導上面講,自己下面睡。睡得天昏地暗,時過境遷,居然還在叨叨叨,自己就拿起酒店提供的紙和筆找這些數字間的祕密。首先,這幾組數字都是偶逆序列,前一晚寫的調整奇逆序列為偶逆序列的程式碼是沒有問題的,那問題出在哪了。抓腦摳鼻,抖腳搖頭,那一組圖片來回翻閱,靈光一閃,水哥附身,原來是這樣:空格項都出現在第三行,哦,不,應該是奇數行。為什麼呢?又去百度,又看到了上面知乎和豆瓣的正經說瞎話的大神(此生最討此類人,害死個仙人),看到這,我開始懷疑,這句話的正確性。奇數階,格子上下左右移動確實不會改變數列的逆序奇偶性。但偶數階,格子的上下移動是會改變序列的奇偶性的,簡單總結一下:

奇數階(3x3,5x5):上移或下移一個數字,其調換的位置是偶數,所以不改變數列逆序數的奇偶性,所以奇數階,生成的初始隨機數列的逆序數必須為偶數;

偶數階(4x4,6x6):上移或下移一個數字,其調換的位置是奇數,所以會改變數列逆序數的奇偶性,上下交換一次改變一次奇偶性,交換兩次就回到初始狀態。所以可以大致這樣理解,偶數的平方仍然為偶數,其有數字的滑塊個數為奇數個,所以有一個數字必然會和空滑塊產生位置交換,如果空滑塊位於奇數行(空滑塊是不參於數字序列的逆序數計算的),就會產生2n-1次交換,其會改變數列逆序數的奇偶性;而位於偶數行,就會產生2n次交換,不會改變數列逆序數的奇偶性,所以用一個公式總結就是:(數列初始狀態是否為偶數) === (空行是否為偶數),簡單來講就是求這兩個數的異或。最後的程式碼流程的實現

clipboard.png

講一講裡面演算法的實現

生成一個亂序不重複的1~n陣列數列

方法有很多,但我知道的兩種,這裡分享一下,有知道其他的,請留言做個評論,讓大家一起進步。 順序陣列隨機性調換 思路基本就是,先生成一個順序的1~n的順序陣列,然後再通過一個1~n的迴圈來打亂這個陣列,其時間複雜度是O(n)。程式碼如下。

    export const disorganize = (length) => {
        const arr = [];
        let temp;
        for (var i = 1; i < length; i++) {
            arr.push(i);
        }
        for (i = 0; i < length; i++) {
            let random = Math.round(Math.random() * (length - 2));
            temp = arr[random];
            arr[random] = arr[i];
            arr[i] = temp;
        }
        return arr;
    };
複製程式碼

隨機數生成亂序陣列 生成一個隨機數,並判斷其是否在目標陣列中已存在,當陣列個數為n時,目的達到。其時間複雜度我不知,程式碼如下:

    export const randomArr = (length) => {
        const arr = [];
        let temp;
        while(arr.length<(length-1)){
            let random = 1+Math.round(Math.random() * (length - 2));
            if(random<length && arr.indexOf(random)===-1){
                arr.push(random);
            }
        }
        return arr;
    };
複製程式碼

雖然程式碼看起比上面的簡單,但其時間複雜度最由是O(f(n),最差情況未知,所以,方法一更推薦,如果說的有什麼毛病,還請及時指出。

數列逆序性的奇偶性判斷

最先想到的就是氣泡排序,因為氣泡排序過程就是判斷兩個數是否逆序,是,就交換,不是,繼續下一組判斷。所以,我們直接將交換的次數,記為數列逆序數個數,就達到了想要的效果。當然這個題用其他排序方法也能達到目的,理解了其排序的原理,就很容易計算數列的逆序性,我這裡是直接用的以前氣泡排序的演算法。

const bubbleOrder=(arr)=>{
    let i ,j ,count=0;
    const swap=(tar,lastIndex,newIndex)=>{
        let temp = tar[lastIndex];
        tar[lastIndex] =tar[newIndex];
        tar[newIndex] = temp;
        count++;
    }
    for(i=0;i<arr.length;i++){
        for(j=arr.length-1;j>i;j--){
            (arr[j]<arr[j-1])&&swap(arr,j-1,j);
        }
    }
    return count;
}
複製程式碼

JS寫一個秒錶

秒錶是啥,start-pause-stop-reset,中間的步驟不是必須的,但前後兩步必須。當然方法有很多,但都離不開setTimeout或則setInterval兩個方法,requestAnimation應該也可以。這裡提供一個自己寫的,當然思路來源於網上,只是用自己的思路表達出來。基於setInterval和Date物件。原始碼如下:

const timer=(offsetTime)=>{  //offsetTime為0時,表示從0開始計,不為0,表示是暫停後繼續計時
    const formatter=(t)=>{
        const res =t>9 ? t : '0'+t
        return res;
    }
    let startTime = new Date().getTime(),tPass=0,tOffset=offsetTime||0;
    this.interId = setInterval(()=> {   //this.interId 是元件下面建的一個保持定時器值的,用於暫停和停止
        let tNew = new Date().getTime(),ms,sec,min,timeStr;
        tPass = tOffset +tNew - startTime;
        ms = Math.floor(tPass/10 % 100);
        sec = Math.floor((tPass / 1000) % 60);
        min = Math.floor((tPass / 1000 / 60) % 60);
        timeStr = formatter(min)+':'+formatter(sec)+':'+formatter(ms);
        this.tick(timeStr,tPass);
    },100)
}

tick(timeStr,tPass){  //這是遊戲頁面一個react元件中的一個用於更新顯示dom的觸發器
    this.setState({
        timePass:tPass,
        time:timeStr
    })
}
複製程式碼

React的使用感受及易錯點(大神留步)

用Vue與用React的區別,抱頭痛哭,Vue半年沒上專案了,忘得差不多了,個人觀點(非喜勿噴)。

  • 直接感受就是,Vue確實比React容易上手,其模板,js,css的元件式的開發方式,更接近我們以前工作中常用的template + requireJs的開發模式。
  • React更強調js的程式設計和專案的整體架構,state放在哪一級,哪一級通過props來控制,當然route 4.0的元件化設計更強調這一點;
  • React在國內還是不像Vue那麼大眾,比方說,出了問題,很多隻有在stackOverflow有相關答案;
  • 不過,不論是Vue還是React,熟悉ES6和麵向物件的程式設計,兩者上手都是很快的(裝個逼,別打我)。

在整個學習過程中,將很多教程中敲黑板指出來的坑,又結結實實踩了一遍,現在可以說影響深刻。自己整理了一下,做個小分享,願和我一樣剛入門的,遇見下面的錯誤,不會那麼迷茫 不要在

Cannot read property 'setState' of undefined'

解釋:這個問題,主要是元件物件的構造constructor中,未在constructor繫結事件處理函式的this指向。這個在教程中是有明確說明的,解決辦法就是constructor()中新增:this.resetClick = this.resetClick.bind(this);

clipboard.png

Cannot read property 'size' of undefined'

解釋:這個問題,主要是元件物件的構造constructor中,未傳入props物件,導致整個元件物件無props屬性;其實除了理解繼承,理解React元件的生命週期也很重要。

clipboard.png

you are adding a new property in the Synthetic event Object

解釋:可以簡單理解為SyntheticEvent是react為瀏覽器相容寫的一個dom事件代理,除了她原有的那些屬性,你不能私自為其新增屬性。

clipboard.png
clipboard.png

其他,後面持續補充

一個疑問

一開始我的遊戲盒子是用的flex佈局,但一考慮,盒子裡面的方塊要滑動效果,我要做滑動的緩動效果,於是又改用了絕對定位佈局,每個 方塊計算其定位點。但事實證明,我當時確實太菜,我用了state來管理每個方塊在盒子的位置,但我調整state時,React的virtualDom 會自動計算,並更新dom節點,那我保持整個專案,怎麼才能自己做出緩動效果呢?純CSS不行,請各位大神給點建議。

結束語

在放假的前幾個小時,把拖了幾天的文章寫完,有點趕,有不足的地方還請及時指出。關於這個專案,後期自己想繼續優化,做一些功能擴充,比如接入資料記錄,NxM階這樣的玩法,有思路的,還請能分享給我,郵箱:closertb@163.com。在最後,送給2017未曾放棄努力的自己一些鼓勵,願2018年能用更好的發展。也祝各位戰友2018新年快樂,年後再見!!!!!

相關文章