試玩鏈接:五子棋
效果圖如下:
與上個版本相比,主要實現了兩個功能:
一個是處理的音效消失的bug,另一個是新增了人機模式。
先說說背景音效的問題,其實解決方案很簡單,就是把中文的MP3音樂名改成英文名字即可。
直接上代碼:
HTML代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋</title>
<link rel="stylesheet" href="css/gobang.css">
</head>
<body>
<a href="javascript:void(0)" id="computerPlay">人機對戰</a>
<a href="" id="newGame">重開一局</a>
<a href="javascript:void(0)" id="personPlay">雙人對戰</a>
<canvas id="canvas" width="480" height="480">
</canvas>
<audio class="audio">
<source src="cbgm.mp3" type="audio/mp3" />
</audio>
<audio class="music">
<source src="bgm.mp3" type="audio/mp3" />
</audio>
<script src="js/game.js"></script>
<script src="js/ai.js"></script>
<script src="js/type.js"></script>
</body>
</html>
由於代碼量變得更多,所以這個版本我採取的外部文件引用的方式,也就是將css代碼以及js代碼都分離開來,這樣也更符合代碼格式標準。
CSS代碼:
body {
margin: 0;
background-color: #ccc;
}
#canvas {
display: block;
position: relative;
margin: 135px auto;
background-color: rgb(221, 168, 21);
}
a{
position: absolute;
width: 100px;
height: 25px;
font-family: Arial;
color: white;
border-radius: 10px;
text-decoration: none;
text-align: center;
}
#computerPlay {
background-color: #f14343;
top: 90px;
left: 36%;
}
#computerPlay:hover {
background: #c90707;
}
#personPlay {
background-color: rgb(57, 190, 243);
top: 90px;
left: 57.5%;
}
#personPlay:hover{
background: rgb(8, 168, 231);
}
#newGame{
background-color: rgb(221, 224, 26);
top: 90px;
left: 47%;
}
#newGame:hover{
background: rgb(198, 201, 40);
}
遊戲開始以及雙人對戰的js代碼:
var canvas = document.querySelector("canvas");
var computerPlay = document.getElementById("computerPlay");
var personPlay = document.getElementById("personPlay");
var chessColor = ['black', 'white'];
var musicStart = false;
var step = 0;
var mapChess = [];
var mode = [
[1, 0],
[0, 1],
[1, 1],
[1, -1]
]
for (var i = 0; i < 15; i++) {
mapChess[i] = [];
for (var j = 0; j < 15; j++) {
mapChess[i][j] = '';
}
}
var ctx = canvas.getContext("2d");/* 獲取繪製環境 */
for (var i = 1; i < 16; i++) {
ctx.moveTo(30 * i, 30);
ctx.lineTo(30 * i, 450);/* 描述繪製路徑 */
ctx.moveTo(30, 30 * i);
ctx.lineTo(450, 30 * i);
}
ctx.stroke();/* 將之前所有的路徑全部繪製一次 */
drawPoint(120, 120);
drawPoint(120, 360);
drawPoint(360, 120);
drawPoint(360, 360);
drawPoint(240, 240);
computerPlay.addEventListener('click', computerStart, false);
personPlay.addEventListener('click', personStart, false);
function personStart() {
canvas.addEventListener('click', start, false);
computerPlay.removeEventListener('click', computerStart, false);
}
function computerStart() {
if (step == 0) {
document.querySelector('.audio').play();
document.querySelector('.music').play();
drawChess(240, 240, 'black');
mapChess[7][7] = 'black';
step++;
}
canvas.addEventListener('click', aiStart, false);
personPlay.removeEventListener('click', personStart, false);
}
function drawChess(x, y, color) {
ctx.fillStyle = color;
ctx.beginPath();/* 提筆 */
ctx.arc(x, y, 13, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
}
function drawPoint(x, y) {
ctx.fillStyle = 'black';
ctx.beginPath();/* 提筆 */
ctx.arc(x, y, 2, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
}
function start(e) {
var audio = document.querySelector('.audio');
var music = document.querySelector('.music');
var color = chessColor[step % 2];
var dx = Math.floor((e.offsetX + 15) / 30) - 1;
var dy = Math.floor((e.offsetY + 15) / 30) - 1;
if (dx < 0 || dx > 14 || dy < 0 || dy > 14) {
return;
}
if (mapChess[dx][dy] == '') {
var audioPromise = document.querySelector('.audio').play();
document.querySelector('.music').play();
music.muted = false;
drawChess((dx + 1) * 30, (dy + 1) * 30, color);
mapChess[dx][dy] = color;
if (judge(dx, dy, color, mode[0], 5) ||
judge(dx, dy, color, mode[1], 5) ||
judge(dx, dy, color, mode[2], 5) ||
judge(dx, dy, color, mode[3], 5)
) {
music.muted = true;
step % 2 == 0 ? alert("黑棋獲勝") : alert("白棋獲勝");
canvas.removeEventListener('click', start, false);
personPlay.removeEventListener('click', personStart, false);
return;
}
if (audioPromise !== undefined) {
audioPromise.then(_ => {
audio.paused = true;
})
.catch(error => {
});
}
step++;
}
}
function judge(x, y, color, mode, number) {
var count = 1;
for (var i = 1; i < number; i++) {
if (mapChess[x + i * mode[0]]) {
if (mapChess[x + i * mode[0]][y + i * mode[1]] == color) {
count++;
} else {
break;
}
}
}
for (var i = 1; i < number; i++) {
if (mapChess[x - i * mode[0]]) {
if (mapChess[x - i * mode[0]][y - i * mode[1]] == color) {
count++;
} else {
break;
}
}
}
return count >= number ? true : false;
}
以上這些代碼除了新添增三個按鈕以及綁定其點擊事件外(這部分看代碼應該很容易理解),其餘部分的代碼在上個版本以及講解過,如果看不明白的夥伴可以參看以下鏈接:前端五子棋第一版
然後就是實現新增的五子棋的ai功能,不考慮ai,其大體實現思路與雙人對戰的模式幾乎一樣,只不過是在玩家下完一步後電腦緊接着再下一步。
電腦下棋的js代碼:
var backX = 0;
var backY = 0;
function aiStart(e) {
var audio = document.querySelector('.audio');
var music = document.querySelector('.music');
var color = chessColor[step % 2];
var dx = Math.floor((e.offsetX + 15) / 30) - 1;
var dy = Math.floor((e.offsetY + 15) / 30) - 1;
if (dx < 0 || dx > 14 || dy < 0 || dy > 14) {
return;
}
if (mapChess[dx][dy] == '') {
var audioPromise = document.querySelector('.audio').play();
document.querySelector('.music').play();
music.muted = false;
//玩家落子
drawChess((dx + 1) * 30, (dy + 1) * 30, color);
mapChess[dx][dy] = color;
if (judge(dx, dy, color, mode[0], 5) ||
judge(dx, dy, color, mode[1], 5) ||
judge(dx, dy, color, mode[2], 5) ||
judge(dx, dy, color, mode[3], 5)
) {
music.muted = true;
canvas.removeEventListener('click', aiStart, false);
computerPlay.removeEventListener('click', computerStart, false);
step % 2 == 0 ? alert("黑棋獲勝") : alert("白棋獲勝");
return;
}
step++;
//電腦落子
AI();
dx = backX;
dy = backY;
drawChess((dx + 1) * 30, (dy + 1) * 30, 'black');
mapChess[dx][dy] = 'black';
if (judge(dx, dy, 'black', mode[0], 5) ||
judge(dx, dy, 'black', mode[1], 5) ||
judge(dx, dy, 'black', mode[2], 5) ||
judge(dx, dy, 'black', mode[3], 5)
) {
music.muted = true;
canvas.removeEventListener('click', aiStart, false);
computerPlay.removeEventListener('click', computerStart, false);
step % 2 == 0 ? alert("黑棋獲勝") : alert("白棋獲勝");
return;
}
if (audioPromise !== undefined) {
audioPromise.then(_ => {
audio.paused = true;
})
.catch(error => {
});
}
step++;
}
}
function AI() {
var computerScore = [];
var personScore = [];
var maxScore = 0;
for (var i = 0; i < 15; i++) {
personScore[i] = [];
computerScore[i] = [];
for (var j = 0; j < 15; j++) {
personScore[i][j] = 0;
computerScore[i][j] = 0;
}
}
for (var i = 0; i < mapChess.length; i++) {
for (var j = 0; j < mapChess.length; j++) {
if (mapChess[i][j] == '') {
var sum = 0;
//一步得分
for (var k = 0; k <= 3; k++) {
var perArr = getType(i, j, 'white', mode[k]);
personScore[i][j] += 4 * Math.pow(10, perArr[1] - perArr[0]);
var comArr = getType(i, j, 'black', mode[k]);
computerScore[i][j] += 6 * Math.pow(10, comArr[1] - comArr[0]);
}
sum = computerScore[i][j] + personScore[i][j];
if (sum > maxScore) {
maxScore = sum;
xMax = i;
yMax = j;
}
}
}
}
backX = xMax;
backY = yMax;
}
在aiStart這個函數裏說明兩點:
- 由於電腦下棋是無需觸發點擊事件,所以我直接將其綁定在玩家下棋的後面。
- 所謂ai下棋,無非就是通過一系列複雜的邏輯計算,得到最終位置的座標,所以我使用全解變量backX ,backY 來接收最終的電腦下棋的位置。
在AI這個函數裏說明兩點:
- 首先需要用computerScore 和personScore 來分別保存棋盤中每個位置的計算得分,所以它們也是和棋盤一樣大的數組。又由於每次下棋後部分位置的得分會改變,所以它們也是局部變量。(正是由於只有部分位置的得分會改變,所以這裏也是算棋功能的優化點)
- 通過getType函數來獲取某個點的一個方向的得分(只有四個方向),這裏我把電腦的得分系數設置爲6而玩家的得分系數設置爲4,因爲下棋的目的是獲勝,所以己方的優勢更重要一些,但是同時敵之要低,我方也需要爭取,這也是爲什麼最後我設置的得分是兩者之間的求和而不是作差。
最後,該亮出最神奇的getType函數:
function getType(x, y, color, mode) {
var countBlock = 0;
var count = 1;
var i = 1;
while ((x + i * mode[0]) < 15 && (y + i * mode[1]) < 15 && mapChess[x + i * mode[0]][y + i * mode[1]] != '') {
if (mapChess[x + i * mode[0]][y + i * mode[1]] == color) {
count++;
} else {
countBlock++;
break;
}
i++;
}
if ((x + i * mode[0]) < 15 || (y + i * mode[1]) < 15) {
countBlock = 4;
}
i = 1;
while ((x - i * mode[0]) >= 0 && (y - i * mode[1]) >= 0 && mapChess[x - i * mode[0]][y - i * mode[1]] != '') {
if (mapChess[x - i * mode[0]][y - i * mode[1]] == color) {
count++;
} else {
countBlock++;
break;
}
i++;
}
if ((x - i * mode[0]) >= 0 || (y - i * mode[1]) >= 0) {
countBlock = 4;
}
return [countBlock, count];
}
在getType這個函數裏說明三點:
- 首先爲什麼說它神奇呢,因爲這個函數包括了五子棋裏面的眠一,活一,死一,眠二,活二,死二眠三,活三,死三,眠四,死四,活四,成五這麼多種單個位置的局面(這裏的死是表示兩端都已被對手的棋子封住),注意這裏並不包含跳眠二,跳活二等跳棋局面的考慮。(但是後面版本我會加上)
- 這裏面還做出了countBlock = 4的邊界考慮,就是爲了避免電腦下無用棋,但是我這裏的設置還是存在問題
- 最終的結果是由count數值減去countBlock數值作爲指數,其實這並不是一個最好的算法,但是是一個很有效率的算法。
總之,此電腦ai依然存在不少bug,但是就單步下棋水平而言,我個人感覺已經馬馬虎虎了。後續的重點將是給與電腦算棋甚至是算殺的功能。這部分算法的難度很大,我目前暫時還沒有這個能力,所以還得更加持續學習,我目前有個思路就是回溯剪枝算法。這裏我很歡迎大佬們評論區留言賜教!