背景の紹介
私は以前に C# winform で gdi+ と Java グラフィック パッケージを使ってバックギャモンをプレイしたことがあったので、この論理的な考え方には精通していましたが、最近、HTML5 のキャンバス描画機能 (通常、企業はこれらを使用しません) をレビューしたいと思いました。もちろん、以前の五目並べのクライアントコードを参照したわけではなく、以前の勝敗判定アルゴリズムとコンピューターAI(ネットワーク借用)アルゴリズムを使用しました。現在は HTML5 と Baidu で作られていますが、自分で実装するのは常に良いことです。ナンセンスはこれくらいにして、本題に入りましょう。 ^_^
インターフェース機能と今後追加される可能性のある機能の紹介
現在のインターフェース機能:
主なインターフェースには
1:人間対人間、または人間対機械の戦闘オプション 2: チェスの駒の外観の選択 3: チェス盤の背景 選択肢 4: ボードの線の色の選択
ゲームインターフェイスには以下が含まれます
1: プレイヤー名 2: プレイヤーの駒 3: 現在チェスをプレイしている人 背景の位置 4: プレイヤーのスコア 5 : 機能メニューエリア (再開と無制限の後悔) 6 : チェス盤エリア 7. 勝利後のチェスの駒の接続 8. 最後のチェスの位置の点滅表示 9. カーソルの位置決め
ゲーム終了インターフェース
1: 勝利の背景画像 2:勝利プレイヤー名 3: 次のボタンに進む
機能追加可能
1. メインインターフェースに戻る 2. チェスゲームと関連データを保存 3. チェスゲームと関連データを読み込む 4. スワップ役割 5. オンライン対戦 (2 台) 6. 双方の思考の合計時間を記録します
インターフェースのスクリーンショットの感謝 (美化する時間を割いていませんでした、かなり醜いです)
全体的なデザインとメインコードの紹介
全体的なデザイン
チェスのプレイプロセス: プレイヤーまたはコンピューター AI がチェスをプレイします ---> チェスの駒を描画します --->チェスの駒の二次元座標値 ----> ロジック (論理的判断) ----> (プレイヤー) 片面 5 つの駒 ---> 勝ちインターフェース
↑ ↓
悔い改めのプロセス(全員でプレイ)
: 1 人のプレイヤーがその手を後悔しています ----> チェスのレコード スタックをポップアップし、それを最後の 1 つのチェスの駒に設定します --->最後の駒の画像をクリア ---> 駒の 2 次元座標値をクリア ---> 最後の駒の位置とフラッシュを再配置して表示悔い改めの過程(人機戦) :
ファングはチェスをプレイします ----> チェスの記録スタックを 2 回ポップアップし、最後のコンピュータを最後のチェスとして設定します ---> 2 つのポップアップ記録画像をクリアします ---> 2 各チェスの駒の 2 次元座標値 ---> 最後のチェスの位置とフラッシュを表示するために再配置しますメインコードの紹介
メインコードは 2 つのブロックに分かれています: 1. インターフェースロジックブロック 2. ゲーム本体ブロック (インターフェイスがゲームコードから分離され、ロジックが明確で、分業が明確)
シミュレーションイベント
通知: ゲーム本体のロジックブロック、各結果インタラクションのためにインターフェイス層に通知されます (C# や Java の委任やイベントと同様)<script type="text/javascript"> var gb = null; var infoboj = document.getElementsByClassName("info")[0]; var pl1obj = document.getElementById("pl1"); var pl2obj = document.getElementById("pl2"); var plname1obj = document.getElementById("plname1"); var plname2obj = document.getElementById("plname2"); var chesstypeobj = document.getElementsByName("chesstype"); var chesscolorobj = document.getElementsByName("chesscolor"); var chessbgObj = document.getElementsByName("chessbg"); var winerpnl = document.getElementById("winer"); document.getElementById("startgame").addEventListener("click", function() { function initParams() { var chessTypeValue = 1; if (chesstypeobj.length > 0) { for (var i = 0; i < chesstypeobj.length; i++) { if (chesstypeobj[i].checked) { chessTypeValue = chesstypeobj[i].value; break; } } } var linevalue = ""; if (chesscolorobj.length > 0) { for (var i = 0; i < chesscolorobj.length; i++) { if (chesscolorobj[i].checked) { linevalue = chesscolorobj[i].value; break; } } } var bcorimgvalue = ""; if (chessbgObj.length > 0) { for (var i = 0; i < chessbgObj.length; i++) { if (chessbgObj[i].checked) { bcorimgvalue = chessbgObj[i].value; break; } } } return { lineColor: linevalue, chessType: chessTypeValue, //1 色彩棋子 2 仿真棋子 playAName: plname1Input.value, playBName: plname2Input.value, backColorORImg: bcorimgvalue, playAImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playA.png", playBImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playB.png", playerBIsComputer:openComputer.checked }; } document.getElementById("cc").style.display = "block"; gb = new gobang(initParams()); /** * 设置一些界面信息 * @param {Object} opt */ gb.info = function(opt) { infoboj.style.visibility = "visible"; document.getElementsByClassName("startpnl")[0].style.visibility = "hidden"; plname1obj.innerHTML = opt.playAName; plname2obj.innerHTML = opt.playBName; if (opt.chessType == 1) { var span1 = document.createElement("span"); pl1obj.insertBefore(span1, plname1obj); var span2 = document.createElement("span"); pl2obj.insertBefore(span2, plname2obj); } else { var img1 = document.createElement("img"); img1.src = opt.playAImg; pl1obj.insertBefore(img1, plname1obj); var img2 = document.createElement("img"); img2.src = opt.playBImg; pl2obj.insertBefore(img2, plname2obj); } } /** * 每次下棋后触发事件 * @param {Object} c2d */ gb.operate = function(opt, c2d) { if (!c2d.winer || c2d.winer <= 0) { pl1obj.removeAttribute("class", "curr"); pl2obj.removeAttribute("class", "curr"); if (c2d.player == 1) { pl2obj.setAttribute("class", "curr"); } else { pl1obj.setAttribute("class", "curr"); } document.getElementById("backChessman").innerHTML="悔棋("+c2d.canBackTimes+")"; } else { var winname = c2d.winer == 1 ? opt.playAName : opt.playBName; var str = "恭喜,【" + winname + "】赢了!" alert(str); winerpnl.style.display = "block"; document.getElementById("winerName").innerHTML = "恭喜,【" + winname + "】赢了!"; document.getElementById("pl" + c2d.winer).style.backgroundColor = "pink"; document.getElementById("scoreA").innerHTML = c2d.playScoreA; document.getElementById("scoreB").innerHTML = c2d.playScoreB; } } gb.start(); }); document.getElementById("openComputer").addEventListener("change", function() { if (this.checked) { plname2Input.value = "电脑"; plname2Input.disabled = "disabled"; } else { plname2Input.value = "玩家二"; plname2Input.disabled = ""; } }); //document.getElementById("openComputer").checked="checked"; //重新开始 function restartgui() { if (gb) { winerpnl.style.display = "none"; pl1obj.removeAttribute("class", "curr"); pl2obj.removeAttribute("class", "curr"); document.getElementById("pl1").style.backgroundColor = ""; document.getElementById("pl2").style.backgroundColor = ""; gb.restart(); } }; </script>
ゲームのメイン コード ブロック (
function// ========== // =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个交叉的 活二 , 因为是断掉的所以没有纳入算法权重计算,如果加入这个算法,估计很难下赢电脑了。
如符号图:
* *
* *
空位
下这里
因为不是连续的,所有没有纳入。
以上がHTML5 Canvas はバックギャモン ゲーム (画像とテキスト) のコード共有を実装します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。