由於以前用c#的winform中的gdi+,java圖形包作過五子棋,因此作這個邏輯思路也就得心應手,然而最近想溫故html5的canvas繪圖功能(公司通常不用這些),因此作了個五子棋,固然沒參考以前的客戶端代碼,只用使用以前計算輸贏判斷算法和電腦AI(網絡借取)的算法,固然如今html5作的五子棋百度一下很是多,可是本身實現一邊總歸是好事情,好了廢話很少說了進入正題。^_^javascript
目前界面功能:html
主界面包含html5
1:人人、人機對戰選項 2:棋子外觀選擇 3:棋盤背景選擇 4:棋盤線條顏色選擇java
遊戲界面包含git
1:玩家名稱 2:玩家棋子 3:當前由誰下棋背景定位 4:玩家比分 5:功能菜單區域(從新開始和無限悔棋) 6:棋盤區域 7.勝利後連環棋子鏈接 8.最後下棋位置閃爍顯示 9.光標定位github
遊戲結束界面算法
1:勝利背景圖 2:勝利玩家姓名 3:繼續下一把按鈕canvas
可增長功能c#
1.返回主界面 2.保存棋局和相關數據 3.讀取棋局和相關數據 4.交換角色 5.網絡對戰(2臺機器)6.雙方思考總時間記錄數組
http://sandbox.runjs.cn/show/pl3fyuy4 (注意:沒有加棋子圖片下載提示,若是使用仿真棋子,出現下棋爲空,請等待棋子圖片下載完畢)
總體設計
下棋流程:玩家or電腦AI下棋 ---> 繪製棋子 ---> 設定棋子二維座標值 ----> logic(邏輯判斷) ----> (玩家)一方五子連環 ---> 獲勝界面
↑ |
| ↓
<--------------------------------------------------------------------------------------------沒有五子
悔棋流程(人人對戰):一方玩家悔棋 ----> 彈出下棋記錄堆棧並設定爲它是最後一枚棋 ---> 清除最後一枚棋子圖像 ---> 清除棋子二維座標值---> 從新定位顯示最後下棋位置並閃爍
悔棋流程(人機對戰):玩方悔棋 ----> 彈出下棋記錄堆棧2次,設定上一次電腦爲最後一枚棋 ---> 清除彈出的2次記錄圖像 ---> 清除棋子2個棋子二維座標值---> 從新定位顯示最後下棋位置並閃爍
主代碼介紹
主代碼分爲二塊: 1.界面邏輯塊 2.遊戲主體塊 (界面與遊戲代碼分離,邏輯清晰,分工明確)
模擬事件通知:遊戲主體邏輯塊,每次結果都會通知到界面層來進行交互(相似於c#或者java的委託或事件)
界面邏輯代碼
1 <script type="text/javascript"> 2 var gb = null; 3 var infoboj = document.getElementsByClassName("info")[0]; 4 var pl1obj = document.getElementById("pl1"); 5 var pl2obj = document.getElementById("pl2"); 6 var plname1obj = document.getElementById("plname1"); 7 var plname2obj = document.getElementById("plname2"); 8 var chesstypeobj = document.getElementsByName("chesstype"); 9 var chesscolorobj = document.getElementsByName("chesscolor"); 10 var chessbgObj = document.getElementsByName("chessbg"); 11 var winerpnl = document.getElementById("winer"); 12 document.getElementById("startgame").addEventListener("click", function() { 13 14 function initParams() { 15 var chessTypeValue = 1; 16 if (chesstypeobj.length > 0) { 17 for (var i = 0; i < chesstypeobj.length; i++) { 18 if (chesstypeobj[i].checked) { 19 chessTypeValue = chesstypeobj[i].value; 20 break; 21 } 22 } 23 } 24 var linevalue = ""; 25 if (chesscolorobj.length > 0) { 26 for (var i = 0; i < chesscolorobj.length; i++) { 27 if (chesscolorobj[i].checked) { 28 linevalue = chesscolorobj[i].value; 29 break; 30 } 31 } 32 } 33 var bcorimgvalue = ""; 34 if (chessbgObj.length > 0) { 35 for (var i = 0; i < chessbgObj.length; i++) { 36 if (chessbgObj[i].checked) { 37 bcorimgvalue = chessbgObj[i].value; 38 break; 39 } 40 } 41 } 42 return { 43 lineColor: linevalue, 44 chessType: chessTypeValue, //1 色彩棋子 2 仿真棋子 45 playAName: plname1Input.value, 46 playBName: plname2Input.value, 47 backColorORImg: bcorimgvalue, 48 playAImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playA.png", 49 playBImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playB.png", 50 playerBIsComputer:openComputer.checked 51 }; 52 } 53 document.getElementById("cc").style.display = "block"; 54 gb = new gobang(initParams()); 55 /** 56 * 設置一些界面信息 57 * @param {Object} opt 58 */ 59 gb.info = function(opt) { 60 infoboj.style.visibility = "visible"; 61 document.getElementsByClassName("startpnl")[0].style.visibility = "hidden"; 62 plname1obj.innerHTML = opt.playAName; 63 plname2obj.innerHTML = opt.playBName; 64 if (opt.chessType == 1) { 65 var span1 = document.createElement("span"); 66 pl1obj.insertBefore(span1, plname1obj); 67 var span2 = document.createElement("span"); 68 pl2obj.insertBefore(span2, plname2obj); 69 } else { 70 var img1 = document.createElement("img"); 71 img1.src = opt.playAImg; 72 pl1obj.insertBefore(img1, plname1obj); 73 var img2 = document.createElement("img"); 74 img2.src = opt.playBImg; 75 pl2obj.insertBefore(img2, plname2obj); 76 } 77 } 78 /** 79 * 每次下棋後觸發事件 80 * @param {Object} c2d 81 */ 82 gb.operate = function(opt, c2d) { 83 if (!c2d.winer || c2d.winer <= 0) { 84 pl1obj.removeAttribute("class", "curr"); 85 pl2obj.removeAttribute("class", "curr"); 86 if (c2d.player == 1) { 87 pl2obj.setAttribute("class", "curr"); 88 } else { 89 pl1obj.setAttribute("class", "curr"); 90 } 91 document.getElementById("backChessman").innerHTML="悔棋("+c2d.canBackTimes+")"; 92 } else { 93 var winname = c2d.winer == 1 ? opt.playAName : opt.playBName; 94 var str = "恭喜,【" + winname + "】贏了!" 95 alert(str); 96 winerpnl.style.display = "block"; 97 document.getElementById("winerName").innerHTML = "恭喜,【" + winname + "】贏了!"; 98 document.getElementById("pl" + c2d.winer).style.backgroundColor = "pink"; 99 document.getElementById("scoreA").innerHTML = c2d.playScoreA; 100 document.getElementById("scoreB").innerHTML = c2d.playScoreB; 101 } 102 } 103 gb.start(); 104 }); 105 106 document.getElementById("openComputer").addEventListener("change", function() { 107 if (this.checked) { 108 plname2Input.value = "電腦"; 109 plname2Input.disabled = "disabled"; 110 } else { 111 plname2Input.value = "玩家二"; 112 plname2Input.disabled = ""; 113 } 114 }); 115 116 //document.getElementById("openComputer").checked="checked"; 117 118 //從新開始 119 function restartgui() { 120 if (gb) { 121 winerpnl.style.display = "none"; 122 pl1obj.removeAttribute("class", "curr"); 123 pl2obj.removeAttribute("class", "curr"); 124 document.getElementById("pl1").style.backgroundColor = ""; 125 document.getElementById("pl2").style.backgroundColor = ""; 126 gb.restart(); 127 } 128 }; 129 </script>
遊戲主體代碼塊(只包含函數聲明代碼)
// ========== // =name:gobang 遊戲 // =anthor:jasnature // =last modify date:2016-04-13 // ========== (function(win) { var gb = function(option) { var self = this, canObj = document.getElementById("cc"), can = canObj.getContext("2d"); self.contextObj = canObj; self.context = can; if (!self.context) { alert("瀏覽器不支持html5"); return; }; self.Opt = { lineColor: "green", chessType: 1, //1 色彩棋子 2 仿真棋子 playAName: "play1", playBName: "play2", playAColor: "red", playBColor: "blue", playAImg: "img/playA.png", playBImg: "img/playB.png", backColorORImg: "default", playerBIsComputer: false }; self.operate; //合併屬性 for (var a in option) { //console.log(opt[a]); self.Opt[a] = option[a]; }; //私有變量 var my = {}; my.enableCalcWeightNum = false; //顯示AI分數 my.gameover = false; //棋盤相關 my.baseWidth = 30; my.lastFocusPoint = {}; //鼠標最後移動到的座標點,計算後的 my.cw = self.contextObj.offsetWidth; //棋盤寬 my.ch = self.contextObj.offsetHeight; //高 my.xlen = Math.ceil(my.cw / my.baseWidth); //行數 my.ylen = Math.ceil(my.ch / my.baseWidth); //列 my.chessRadius = 14; //棋子半徑 my.playerBIsComputer = false; //棋手B是不是電腦 my.ComputerThinking = false; //電腦是否在下棋 my.goBackC2dIsComputer = false; //最後下棋是否爲電腦 my.switcher = 1; //由誰下棋了 1-a 2-b or computer my.winer = -1; //贏家,值參考my.switcher my.playScoreA = 0; my.playScoreB = 0; //x,y 正方形數量(20*20) my.rectNum = my.xlen; //存儲已下的點 my.rectMap = []; my.NO_CHESS = -1; //沒有棋子標識 my.goBackC2d = {}; //最後下的數組轉換座標 my.downChessmanStackC2d = []; // 記錄已下棋子的順序和位置,堆棧 my.focusFlashInterval = null; //焦點閃爍線程 my.focusChangeColors = ["red", "fuchsia", "#ADFF2F", "yellow", "purple", "blue"]; my.eventBinded = false; my.currChessBackImg = null; my.currChessAImg = null; my.currChessBImg = null; my.currDrawChessImg = null; my.ChessDownNum = 0; //2個玩家 下棋總數 /** * 開始遊戲 */ self.start = function() { }; /** * 從新開始遊戲 */ self.restart = function() { }; /** * 悔棋一步 ,清棋子,並返回上一次參數 */ self.back = function() { } /** * 初始化一些數據 */ function init() { } // self.paint = function() { // // //window.requestAnimationFrame(drawChessboard); // }; /** * 遊戲邏輯 */ function logic(loc, iscomputer) { }; /** * 判斷是否有玩家勝出 * @param {Object} c2d */ function isWin(c2d) { return false; } /** * 鏈接贏家棋子線 * @param {Object} points */ function joinWinLine(points) { } /** * 畫棋盤 */ function drawChessboard() { }; /** * 畫棋子 * @param {Object} loc 鼠標點擊位置 */ function drawChessman(c2d) { } function drawRect(lastRecord, defColor) { } /** * 閃爍最後下棋點 */ function flashFocusChessman() { } /** * 清棋子 * @param {Object} c2d */ function clearChessman() { } /** * @param {Object} loc * @return {Object} I 二維數組橫點(),J二維數組縱點,IX 橫點起始座標,JY縱點起始座標,player 最後下棋玩, winer 贏家 */ function calc2dPoint(loc) { var txp = Math.floor(loc.x / my.baseWidth), typ = Math.floor(loc.y / my.baseWidth) dxp = txp * my.baseWidth, dyp = typ * my.baseWidth; loc.I = txp; loc.J = typ; loc.IX = dxp; loc.JY = dyp; return loc; } my.isChangeDraw = true; /** * 位置移動光標 * @param {Object} loc */ function moveFocus(loc) { } /** * 綁定事件 */ function bindEvent() { if (!my.eventBinded) { self.contextObj.addEventListener("touchstart", function(event) { //console.log(event); var touchObj = event.touches[0]; eventHandle({ s: "touch", x: touchObj.clientX - this.offsetLeft, y: touchObj.clientY - this.offsetTop }) }); self.contextObj.addEventListener("click", function(event) { //console.log("click event"); eventHandle({ s: "click", x: event.offsetX, y: event.offsetY }) }); self.contextObj.addEventListener("mousemove", function(event) { //console.log("mousemove event"); moveFocus({ x: event.offsetX, y: event.offsetY }); }); my.eventBinded = true; } function eventHandle(ps) { if (!my.gameover && !my.ComputerThinking) { logic(ps); if (my.playerBIsComputer && my.switcher == 2) { my.ComputerThinking = true; var pp = AI.analysis(my.goBackC2d.I, my.goBackC2d.J); logic({ I: pp.x, J: pp.y }, true); my.ComputerThinking = false; } } event.preventDefault(); event.stopPropagation(); return false; } } }; win.gobang = gb; })(window);
玩家OR電腦勝出算法
/** * 判斷是否有玩家勝出 * @param {Object} c2d */ function isWin(c2d) { //四個放心計數 豎 橫 左斜 右斜 var hcount = 0, vcount = 0, lbhcount = 0, rbhcount = 0, temp = 0; var countArray = []; //左-1 for (var i = c2d.I; i >= 0; i--) { temp = my.rectMap[i][c2d.J]; if (temp < 0 || temp !== c2d.player) { break; } hcount++; countArray.push({ I: i, J: c2d.J }); } //右-1 for (var i = c2d.I + 1; i < my.rectMap.length; i++) { temp = my.rectMap[i][c2d.J]; if (temp < 0 || temp !== c2d.player) { break; } hcount++; countArray.push({ I: i, J: c2d.J }); } if (countArray.length < 5) { countArray = []; //上-2 for (var j = c2d.J; j >= 0; j--) { temp = my.rectMap[c2d.I][j]; if (temp < 0 || temp !== c2d.player) { break; } vcount++; countArray.push({ I: c2d.I, J: j }); } //下-2 for (var j = c2d.J + 1; j < my.rectMap[c2d.I].length; j++) { temp = my.rectMap[c2d.I][j]; if (temp < 0 || temp !== c2d.player) { break; } vcount++; countArray.push({ I: c2d.I, J: j }); } } if (countArray.length < 5) { countArray = []; //左上 for (var i = c2d.I, j = c2d.J; i >= 0, j >= 0; i--, j--) { if (i < 0 || j < 0) break; temp = my.rectMap[i][j]; if (temp < 0 || temp !== c2d.player) { break; } lbhcount++; countArray.push({ I: i, J: j }); } //右下 if (c2d.I < my.rectMap.length - 1 && c2d.I < my.rectMap[0].length - 1) { for (var i = c2d.I + 1, j = c2d.J + 1; i < my.rectMap.length, j < my.rectMap[0].length; i++, j++) { if (i >= my.rectMap.length || j >= my.rectMap.length) break; temp = my.rectMap[i][j]; if (temp < 0 || temp !== c2d.player) { break; } lbhcount++; countArray.push({ I: i, J: j }); } } } if (countArray.length < 5) { countArray = []; //右上 for (var i = c2d.I, j = c2d.J; i < my.rectMap.length, j >= 0; i++, j--) { if (i >= my.rectMap.length || j < 0) break; temp = my.rectMap[i][j]; if (temp < 0 || temp !== c2d.player) { break; } rbhcount++; countArray.push({ I: i, J: j }); } //左下 if (c2d.I >= 1 && c2d.J < my.rectMap[0].length - 1) { for (var i = c2d.I - 1, j = c2d.J + 1; i > 0, j < my.rectMap[0].length; i--, j++) { if (j >= my.rectMap.length || i < 0) break; temp = my.rectMap[i][j]; if (temp < 0 || temp !== c2d.player) { break; } rbhcount++; countArray.push({ I: i, J: j }); } } } if (hcount >= 5 || vcount >= 5 || lbhcount >= 5 || rbhcount >= 5) { my.winer = c2d.player; my.gameover = true; joinWinLine(countArray); return true; } return false; }
算法簡介:主要思路是搜索最後落下棋子的位置(二維座標)計算 米 字形線座標,看是否有連續5個或以上棋子出現。
鏈接贏家棋子線
/** * 鏈接贏家棋子線 * @param {Object} points */ function joinWinLine(points) { points.sort(function(left, right) { return (left.I + left.J) > (right.I + right.J); }); var startP = points.shift(); var endP = points.pop(); var poffset = my.baseWidth / 2; can.strokeStyle = "#FF0000"; can.lineWidth = 2; can.beginPath(); var spx = startP.I * my.baseWidth + poffset, spy = startP.J * my.baseWidth + poffset; can.arc(spx, spy, my.baseWidth / 4, 0, 2 * Math.PI, false); can.moveTo(spx, spy); var epx = endP.I * my.baseWidth + poffset, epy = endP.J * my.baseWidth + poffset; can.lineTo(epx, epy); can.moveTo(epx + my.baseWidth / 4, epy); can.arc(epx, epy, my.baseWidth / 4, 0, 2 * Math.PI, false); can.closePath(); can.stroke(); }
算法簡介:根據贏家返回的連子位置集合,作座標大小位置排序,直接使用lineto 鏈接 第一個棋子和最後一個
座標換算
/** * 座標換算 * @param {Object} loc * @return {Object} I 二維數組橫點(),J二維數組縱點,IX 橫點起始座標,JY縱點起始座標,player 最後下棋玩, winer 贏家 */ function calc2dPoint(loc) { var txp = Math.floor(loc.x / my.baseWidth), typ = Math.floor(loc.y / my.baseWidth) dxp = txp * my.baseWidth, dyp = typ * my.baseWidth; loc.I = txp; loc.J = typ; loc.IX = dxp; loc.JY = dyp; return loc; }
算法簡介:這個比較簡單,根據每一個格子的寬度計算出實際座標
電腦AI主要代碼(修改來源於網絡)
/** * AI棋型分析 */ AI.analysis = function(x, y) { //若是爲第一步則,在玩家棋周圍一格隨機下棋,保證每一局棋第一步都不同 if (my.ChessDownNum == 1) { return this.getFirstPoint(x, y); } var maxX = 0, maxY = 0, maxWeight = 0, i, j, tem; for (i = BOARD_SIZE - 1; i >= 0; i--) { for (j = BOARD_SIZE - 1; j >= 0; j--) { if (my.rectMap[i][j] !== -1) { continue; } tem = this.computerWeight(i, j, 2); if (tem > maxWeight) { maxWeight = tem; maxX = i; maxY = j; } if (my.enableCalcWeightNum) { can.clearRect(i * 30 + 2, j * 30 + 2, 24, 24); can.fillText(maxWeight, i * 30 + 5, j * 30 + 15, 30); } } } return new Point(maxX, maxY); }; //下子到i,j X方向 結果: 多少連子 兩邊是否截斷 AI.putDirectX = function(i, j, chessColor) { var m, n, nums = 1, side1 = false, //兩邊是否被截斷 side2 = false; for (m = j - 1; m >= 0; m--) { if (my.rectMap[i][m] === chessColor) { nums++; } else { if (my.rectMap[i][m] === my.NO_CHESS) { side1 = true; //若是爲空子,則沒有截斷 } break; } } for (m = j + 1; m < BOARD_SIZE; m++) { if (my.rectMap[i][m] === chessColor) { nums++; } else { if (my.rectMap[i][m] === my.NO_CHESS) { side2 = true; } break; } } return { "nums": nums, "side1": side1, "side2": side2 }; }; //下子到i,j Y方向 結果 AI.putDirectY = function(i, j, chessColor) { var m, n, nums = 1, side1 = false, side2 = false; for (m = i - 1; m >= 0; m--) { if (my.rectMap[m][j] === chessColor) { nums++; } else { if (my.rectMap[m][j] === my.NO_CHESS) { side1 = true; } break; } } for (m = i + 1; m < BOARD_SIZE; m++) { if (my.rectMap[m][j] === chessColor) { nums++; } else { if (my.rectMap[m][j] === my.NO_CHESS) { side2 = true; } break; } } return { "nums": nums, "side1": side1, "side2": side2 }; }; //下子到i,j XY方向 結果 AI.putDirectXY = function(i, j, chessColor) { var m, n, nums = 1, side1 = false, side2 = false; for (m = i - 1, n = j - 1; m >= 0 && n >= 0; m--, n--) { if (my.rectMap[m][n] === chessColor) { nums++; } else { if (my.rectMap[m][n] === my.NO_CHESS) { side1 = true; } break; } } for (m = i + 1, n = j + 1; m < BOARD_SIZE && n < BOARD_SIZE; m++, n++) { if (my.rectMap[m][n] === chessColor) { nums++; } else { if (my.rectMap[m][n] === my.NO_CHESS) { side2 = true; } break; } } return { "nums": nums, "side1": side1, "side2": side2 }; }; AI.putDirectYX = function(i, j, chessColor) { var m, n, nums = 1, side1 = false, side2 = false; for (m = i - 1, n = j + 1; m >= 0 && n < BOARD_SIZE; m--, n++) { if (my.rectMap[m][n] === chessColor) { nums++; } else { if (my.rectMap[m][n] === my.NO_CHESS) { side1 = true; } break; } } for (m = i + 1, n = j - 1; m < BOARD_SIZE && n >= 0; m++, n--) { if (my.rectMap[m][n] === chessColor) { nums++; } else { if (my.rectMap[m][n] === my.NO_CHESS) { side2 = true; } break; } } return { "nums": nums, "side1": side1, "side2": side2 }; }; /** * 計算AI下棋權重 * chessColor 玩家1爲玩家2爲AI */ AI.computerWeight = function(i, j, chessColor) { //基於棋盤位置權重(越靠近棋盤中心權重越大) var weight = 19 - (Math.abs(i - 19 / 2) + Math.abs(j - 19 / 2)), pointInfo = {}; //某點下子後連子信息 //x方向 pointInfo = this.putDirectX(i, j, chessColor); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重 pointInfo = this.putDirectX(i, j, chessColor - 1); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重 //y方向 pointInfo = this.putDirectY(i, j, chessColor); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重 pointInfo = this.putDirectY(i, j, chessColor - 1); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重 //左斜方向 pointInfo = this.putDirectXY(i, j, chessColor); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重 pointInfo = this.putDirectXY(i, j, chessColor - 1); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重 //右斜方向 pointInfo = this.putDirectYX(i, j, chessColor); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重 pointInfo = this.putDirectYX(i, j, chessColor - 1); weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重 return weight; }; //權重方案 活:兩邊爲空可下子,死:一邊爲空 //其實還有不少種方案,這種是最簡單的 AI.weightStatus = function(nums, side1, side2, isAI) { var weight = 0; switch (nums) { case 1: if (side1 && side2) { weight = isAI ? 15 : 10; //一 } break; case 2: if (side1 && side2) { weight = isAI ? 100 : 50; //活二 } else if (side1 || side2) { weight = isAI ? 10 : 5; //死二 } break; case 3: if (side1 && side2) { weight = isAI ? 500 : 200; //活三 } else if (side1 || side2) { weight = isAI ? 30 : 20; //死三 } break; case 4: if (side1 && side2) { weight = isAI ? 5000 : 2000; //活四 } else if (side1 || side2) { weight = isAI ? 400 : 100; //死四 } break; case 5: weight = isAI ? 100000 : 10000; //五 break; default: weight = isAI ? 500000 : 250000; break; } return weight; };
AI分析:這個只是最簡單的算法,其實很簡單,計算每一個沒有下棋座標的分數,也是按照 米 字形 計算,計算格子8個方向出現的 一個棋子 二個棋子 三個棋子 四個棋子,其中還分爲是否被截斷,其實就是邊緣是否被堵死。
其實這個AI算法後續還有不少能夠優化,好比 斷跳 二活 其實就是2個交叉的 活二 , 由於是斷掉的因此沒有歸入算法權重計算,若是加入這個算法,估計很難下贏電腦了。
如符號圖:
* *
* *
空位
下這裏
由於不是連續的,全部沒有歸入。
http://jasnature.github.io/gobang_html5/
有興趣的能夠下載修改並提交代碼進來^_^