原生js實現(xiàn)五子棋

為什突然做這個,因為這是個筆試題,拖了一個月才寫(最近終于閑了O(∩_∩)O)穗慕,廢話不多說,說說這個題吧

題目要求

編寫一個單機【五子棋】游戲妻导,要求如下:
1.使用原生技術(shù)實現(xiàn)逛绵,兼容 Chrome 瀏覽器即可。
2.實現(xiàn)勝負判斷倔韭,并給出贏棋提示术浪;任意玩家贏得棋局,鎖定棋盤寿酌。
3.請盡可能的考慮游戲的擴展性胰苏,界面可以使用 DOM / Canvas 實現(xiàn)》菝考慮后續(xù)切換界面實現(xiàn)的方式成本最低碟联。(比如選擇使用 DOM 實現(xiàn)界面妓美,需求改變?yōu)槭褂?Canvas 實現(xiàn)時盡可能少的改動代碼)。
4.實現(xiàn)一個悔棋功能
5.實現(xiàn)一個撤銷悔棋功能鲤孵。
6.人機對戰(zhàn)部分可選壶栋。
7.盡可能的考慮實現(xiàn)的靈活性和擴展性。

UI部分

棋盤

五子棋普监,首先我們會想到棋盤贵试、棋子,這是棋類游戲的基本元素凯正,實現(xiàn)棋盤毙玻、棋子基本的兩種思路,一種使用html標(biāo)簽?zāi)M廊散,另外一種就是使用canvas去畫桑滩。這里我們選擇使用canvas。

chess
    canvas.strokeStyle = "#ccc";
    for(let i = 0; i<=15; i++){
        canvasA.moveTo(20+40*i, 20);
        canvasA.lineTo(20+40*i, 620);
        canvasA.stroke();
        canvasA.moveTo(20, 20+40*i);
        canvasA.lineTo(620, 20+40*i);
        canvasA.stroke();
    }

主要的思路就是畫等距離的直線允睹,包活橫向和縱向运准,用一個循環(huán)即可。


image.png
棋子

棋子的注意點就是位置缭受,圓心和棋盤的交叉點要對齊胁澳,當(dāng)鼠標(biāo)點擊某個范圍的時候要在對應(yīng)的交叉點為圓心處畫圓。

oChess.addEventListener('click', (event) => {
    let x = Math.floor(event.offsetX/40),
        y = Math.floor(event.offsetY/40);
}, false)

我們以交叉點為中心的正方形(變長為棋格邊長)為范圍米者,也就是說每當(dāng)鼠標(biāo)點擊到紅色區(qū)域就在該中心點畫棋子韭畸。這里獲得中心點坐標(biāo)之后就可以在對應(yīng)點畫棋子


image.png
//畫棋子
const drawChessman = (x, y, temp) => {
    canvas.beginPath();
    canvas.arc(20+x*40,20+y*40, 18, 0, 360);
    canvas.closePath();
    canvas.fillStyle = temp ? "#fff" : "#000";
    canvas.fill();
}
其他

由于有悔棋功能,我們可以使用canvas的clearRect蔓搞,但是胰丁,在清除棋子的時候不僅把棋子清除掉,而且棋子覆蓋的棋盤也被清除掉喂分,這是不被允許的隘马,但是我們總不能把棋盤再重新畫一遍,這樣棋盤就會把棋子覆蓋妻顶,而且總歸是有性能問題。

image.png

這里耍了個小聰明蜒车,我們使用兩個canvas來完成讳嘱,底層的canvas用來畫棋盤,上層的canvas用來畫棋譜酿愧,并將其背景色設(shè)置為透明沥潭,這樣清除棋子就不會影響到棋盤。

算法

界面是有了嬉挡,但是玩的的時候怎么確定誰贏了钝鸽?人機的時候電腦該怎么走汇恤?

贏法

這里使用一種最簡單的算法,那就是先枚舉出所有的贏法拔恰,然后再每走一步的時候都去判斷下棋人贏的權(quán)重因谎。那么有多少種贏的方式呢,當(dāng)然是跟棋盤大小有關(guān)(可不是棋盤的寬高哦??)颜懊,格子有多少決定這有多少贏法财岔。我們都知道只要五個棋子連成一條線即可,這條線可以橫著可以豎著也可以斜著河爹,所以我們就按照這幾個方向來計算即可匠璧,橫、豎咸这、斜夷恍、反斜。

//可以贏的方式
let canWin = [];
//可以贏的種類數(shù)量
let winCount = 0;

//這橫線豎線都是15個點媳维,初始化每個點的贏法酿雪,每個點都有可能在任意方向與其他棋子連成一條線(五子),所以每個點贏的方式構(gòu)成一個數(shù)組
for(let i = 0; i< 15; i++){
    canWin[i] = [];
    for(let j = 0; j<15; j++){
        canWin[i][j] = [];
    }
}
//橫線贏的方式
for(let i = 0; i<15;i++){
        //橫線有十一種不重復(fù)的五個點侨艾,下面同理
    for(let j = 0;j<11; j++){
        //連成五個子
        for(let k = 0; k<5; k++){
            canWin[i][j+k][winCount] = true;
        }
        winCount++;
    }
}
//豎線贏的方式
for(let i = 0; i<11;i++){
    for(let j = 0;j<15; j++){
        //連成五個子
        for(let k = 0; k<5; k++){
            canWin[i+k][j][winCount] = true;
        }
        winCount++;
    }
}
//正斜線贏的方式
for(let i = 0; i<11;i++){
    for(let j = 0;j<11; j++){
        //連成五個子
        for(let k = 0; k<5; k++){
            canWin[i+k][j+k][winCount] = true;
        }
        winCount++;
    }
}
//反斜線贏的方式
for(let i = 0; i<11;i++){
    for(let j = 14;j>3; j--){
        //連成五個子
        for(let k = 0; k<5; k++){
            canWin[i+k][j-k][winCount] = true;
        }
        winCount++;
    }
}

所有的贏法都已經(jīng)統(tǒng)計出來执虹,那么對于電腦下棋來說肯定是要走最有可能贏的位置,那么哪個位置最有可能贏唠梨,當(dāng)然是最有可能連成五個棋子的地方袋励,因此,我們需要計算所有的沒有落子的地方如果落子的話贏得概率大小当叭,我們只需要加個權(quán)重即可

//電腦下棋
const computerStep = () => {
        //人贏得期望值
    let peopleScore = [];
        //電腦贏得期望值
    let computerScore = [];
    let maxScore = 0;
        //電腦落子坐標(biāo)
    let currentX = 0;
    let currentY = 0;
    //每個坐標(biāo)點落子勝利的期望值(初始化)
    for(let i = 0; i<15; i++){
        peopleScore[i] = [];
        computerScore[i] = [];
        for(let j = 0; j<15; j++){
            peopleScore[i][j] = 0;
            computerScore[i][j] = 0;
        }
    }
    for(let i = 0; i<15; i++){
        for(let j = 0; j<15; j++){
            //還未落子
            if(chessArr[i][j] == 0){
                for(let k = 0;k<winCount;k++){
                    if(canWin[i][j][k]){
                                                //peopleWin茬故,computerWin每種贏法人或者電腦落子數(shù)量,如果有別的落子則加10蚁鳖,表示這種贏法不可能贏
                        switch(peopleWin[k]){
                            case 1:peopleScore[i][j]+=100;
                                break;
                            case 2:peopleScore[i][j]+=400;
                                break;
                            case 3:peopleScore[i][j]+=800;
                                break;
                            case 4:peopleScore[i][j]+=2000;
                                break;
                        }
                        switch(computerWin[k]){
                            case 1:computerScore[i][j]+=150;
                                break;
                            case 2:computerScore[i][j]+=450;
                                break;
                            case 3:computerScore[i][j]+=850;
                                break;
                            case 4:computerScore[i][j]+=10000;
                                break;
                        }
                    }
                }
                if(peopleScore[i][j]>maxScore){
                    maxScore = peopleScore[i][j];
                    currentX = i;
                    currentY = j;
                }else if(peopleScore[i][j] == maxScore){
                    if(computerScore[i][j]>computerScore[currentX][currentY]){
                        currentX = i;
                        currentY = j;
                    }
                }
                if(computerScore[i][j]>maxScore){
                    maxScore = computerScore[i][j];
                    currentX = i;
                    currentY = j;
                }else if(computerScore[i][j] == maxScore){
                    if(peopleScore[i][j]>peopleScore[currentX][currentY]){
                        currentX = i;
                        currentY = j;
                    }
                }
            }
        }
    }
    drawChessman(currentX, currentY, false);
    // currentComputer = [currentX, currentY];
        //記錄電腦落子位置
    pTwo[0].push([currentX, currentY]);
    chessArr[currentX][currentY] = 2;
    for(let i = 0; i<winCount; i++){
        if(canWin[currentX][currentY][i]){
            computerWin[i]++;
            //人不可能贏
            peopleWin[i] += 10;
            if(computerWin[i]==5){
                alert('computer win!')
                gameOver = true;
            }
        }
    }

}

主要思路就是磺芭,首先找出棋盤上所有沒有落子的位置,然后計算該點電腦或人落子的話贏得期望值醉箕,分別找出最大值钾腺,那么就是我們需要落子的地方,這時有可能是兩個點讥裤,如果人下一步落子的地方期望值大于電腦放棒,說明需要堵棋,否則不用己英。

判斷輸贏
for(let i = 0; i<winCount; i++){
        if(canWin[currentX][currentY][i]){
            computerWin[i]++;
            //人不可能贏
            peopleWin[i] += 10;
            if(computerWin[i]==5){
                alert('computer win!')
                gameOver = true;
            }
        }
    }

判斷輸贏就很簡單了间螟,走完一步就判斷人或者電腦是否在某一種贏法已經(jīng)落子5顆,當(dāng)然,如果已經(jīng)贏了就不能再落子了厢破。

人對人

人對人的時候就簡單很多了荣瑟,不需要對電腦判斷,只需要切換人即可摩泪,并且要注意一些判斷條件不再是人機笆焰。

oChess.addEventListener('click', (event) => {
    if(gameOver){
        return;
    }
    if(!isp2p){
        if(!isMan){
            return;
        }
    }
    let x = Math.floor(event.offsetX/40),
        y = Math.floor(event.offsetY/40);
    if(chessArr[x][y] == 0){
        drawChessman(x, y, isMan);
        // currentPeople = [x, y];
        //只有當(dāng)前是人對人而且不是第一個人下棋才賦值
        if(isp2p && !isMan){
            pTwo[0].push([x,y]);
        }else{
            pOne[0].push([x, y]);
        }
        chessArr[x][y] =  1;
        for(let i = 0; i<winCount; i++){
            if(canWin[x][y][i]){

                if(isp2p && !isMan){
                    computerWin[i]++;
                    //HOU
                    peopleWin[i] += 10;
                    if(computerWin[i]==5){
                        alert('opponent win!')
                        gameOver = true;
                    }
                }else{
                    peopleWin[i]++;
                    //電腦不可能贏
                    computerWin[i] += 10;
                    if(peopleWin[i]==5){
                        alert('you win!')
                        gameOver = true;
                    }
                }
            }

        }
        if(!gameOver){
            if(!isp2p){
                computerStep();
            }
                        //這個值還代表著棋子顏色的變化
            isMan = !isMan;
        }
    }

})

這里用isp2p判斷是否是人對人,isMan表示是否是人在下棋(這個值還代表著棋子顏色的變化加勤,互不影響的)仙辟,注意點就是一個人下完之后不再是電腦下

悔棋與取消悔棋

之前我們在每次下棋的時候都記錄了雙方的落子位置,所以只需要將最后一個落子位置的棋子清除掉鳄梅,并將落子記錄刪除叠国,而且包括得分等都需要重置,只要按照落子的規(guī)則反著來即可戴尸。

oBack.addEventListener('click', (event) => {
    let currentOne = [];
    let currentTwo = [];
    isBack = true;
        //這里悔棋會刪除落子位置粟焊,但是取消悔棋還需要記錄之前位置
    currentOne = pOne[0].pop();
    pOne[1].push(currentOne);
    currentTwo = pTwo[0].pop();
    pTwo[1].push(currentTwo);
    if(!currentOne){
        return;
    }
    clearChessman(currentOne[0], currentOne[1], true);
    clearChessman(currentTwo[0], currentTwo[1], true);
    chessArr[currentOne[0]][ currentOne[1]] = 0;
    chessArr[currentTwo[0]][ currentTwo[1]] = 0;
    for(let i = 0; i<winCount; i++){
        if(canWin[currentOne[0]][currentOne[1]][i]){
            peopleWin[i]--;
            computerWin[i] -= 10;
        }
    }
    for(let i = 0; i<winCount; i++){
        if(canWin[currentTwo[0]][currentTwo[1]][i]){
            computerWin[i]--;
            peopleWin[i] -= 10;
        }
    }
    gameOver = false;

})

這里悔棋的時候我們把記錄的位置刪除了,但是取消悔棋的時候我們還需要上一步的位置孙蒙,所以每次悔棋的時候都需要把刪除的位置記錄下來项棠,以便取消悔棋。取消悔棋做的呢就跟悔棋相反了挎峦。

注意點:

1.沒有悔棋就沒有取消悔棋香追。這不是廢話嗎?這里要注意的有兩點:第一坦胶,所有悔棋記錄全都取消悔棋就不再悔棋透典;第二:每次下棋之后之前的悔棋就不再算數(shù),需要制空顿苇。因此在取消悔棋完要將變量isBack重置為false,還有就是每次人或電腦下棋都要重置悔棋和取消悔棋記錄

       //重置悔棋部分
   isBack = false;
   pOne = [];
   pTwo = [];

2.取消悔棋也要把每一方的得分重新計算峭咒。

oReback.addEventListener('click', (event) => {
    let currentOne = [];
    let currentTwo = [];

    //如果沒有悔棋就沒有取消悔棋
    if(!isBack){
        return;
    }
    console.log(pTwo[0])
    currentOne = pOne[1].pop();
    pOne[0].push(currentOne);
    currentTwo = pTwo[1].pop();
    pTwo[0].push(currentTwo);
    console.log(pTwo)

    if(!currentOne){
        return;
    }
    //所有悔棋撤銷之后再重制
    if(!pOne[1].length){
        isBack = false;
    }
    drawChessman(currentOne[0], currentOne[1], true);
    chessArr[currentOne[0]][currentOne[1]] =  1;
    for(let i = 0; i<winCount; i++){
        if(canWin[currentOne[0]][currentOne[1]][i]){
            peopleWin[i]++;
            //電腦不可能贏
            computerWin[i] = 10;
            if(peopleWin[i]==5){
                alert('you win!')
                gameOver = true;
            }
        }
    }
    if(!gameOver){
        if(!isp2p){
            //為了防止二次添加到已走位置
            drawChessman(currentTwo[0], currentTwo[1], false);
            chessArr[currentTwo[0]][currentTwo[1]] =  2;
            for(let i = 0; i<winCount; i++){
                if(canWin[currentTwo[0]][currentTwo[1]][i]){
                    computerWin[i]++;
                    //電腦
                    peopleWin[i] += 10;
                    if(computerWin[i]==5){
                        alert('computer win!')
                        gameOver = true;
                    }
                }
            }
        }else{
            drawChessman(currentTwo[0], currentTwo[1], false);
            chessArr[currentTwo[0]][currentTwo[1]] =  2;
            for(let i = 0; i<winCount; i++){
                if(canWin[currentTwo[0]][currentTwo[1]][i]){
                    computerWin[i]++;
                    //先手
                    peopleWin[i] += 10;
                    if(computerWin[i]==5){
                        alert('opponent win!')
                        gameOver = true;
                    }
                }
            }
        }
        isMan = !isMan;
    }

})
重置

這個就很簡單了,我們只需要把棋盤清空纪岁,然后把所有參數(shù)初始化原始值即可凑队,主要用于重新開始游戲以及第一次進入的時候重置棋盤。
洋洋灑灑寫了這么多幔翰,大家慢慢看漩氨,有些地方有點繞,但是沒有很難的算法遗增,所以不會很費勁的才菠。
源碼地址:https://github.com/Stevenzwzhai/stevenzwzhai.github.com/tree/master/FE/src/chess
在線演示:https://stevenzwzhai.github.io/FE/src/chess

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市贡定,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌可都,老刑警劉巖缓待,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚓耽,死亡現(xiàn)場離奇詭異,居然都是意外死亡旋炒,警方通過查閱死者的電腦和手機步悠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘫镇,“玉大人鼎兽,你說我怎么就攤上這事∠吵” “怎么了谚咬?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尚粘。 經(jīng)常有香客問我择卦,道長,這世上最難降的妖魔是什么郎嫁? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任秉继,我火速辦了婚禮,結(jié)果婚禮上泽铛,老公的妹妹穿的比我還像新娘尚辑。我一直安慰自己,他們只是感情好盔腔,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布杠茬。 她就那樣靜靜地躺著,像睡著了一般铲觉。 火紅的嫁衣襯著肌膚如雪澈蝙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天撵幽,我揣著相機與錄音灯荧,去河邊找鬼。 笑死盐杂,一個胖子當(dāng)著我的面吹牛逗载,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播链烈,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼厉斟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了强衡?” 一聲冷哼從身側(cè)響起擦秽,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后感挥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缩搅,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年触幼,在試婚紗的時候發(fā)現(xiàn)自己被綠了硼瓣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡置谦,死狀恐怖堂鲤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媒峡,我是刑警寧澤瘟栖,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站丝蹭,受9級特大地震影響慢宗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奔穿,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一镜沽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贱田,春花似錦缅茉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至耗拓,卻和暖如春拇颅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乔询。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工樟插, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人竿刁。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓黄锤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親食拜。 傳聞我的和親對象是個殘疾皇子鸵熟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容