之前用後端語言實現了一個控制檯應用的簡化五子棋遊戲,效果總是不太好,不能用鼠標操作是硬傷,今兒我們就用最基礎的前端語言來實現一個五子棋遊戲。先看看實現後的頁面效果:
看到頁面顯示不難分析出:
瀏覽器頁面只有中間一塊棋盤區域,很簡單,也很單調。但是這個棋盤的繪製很難用一般的前端技術實現,所以這裏我使用專門的canvas標籤進行操作
給出html與css代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>五子棋</title>
<style>
body {
margin: 0;
background-color: #ccc;
}
#canvas {
display: block;
margin: 135px auto;
background-color: rgb(221, 168, 21);
}
</style>
</head>
<body>
<canvas id="canvas" width="480" height="480">
</canvas>
<video src="棋子音效d.mp3" class="audio"></video>
<video src="Thomas Parisch _ Miles Hankins - 王者榮耀——王者戰歌.mp3" class="music"></video>
</body>
</html>
此html與css代碼應該不難理解,屏幕中間定義一個canvas畫板區域,然後設置了兩個音效,分別作用於落子的聲音與背景音樂。
接下來就是最重要的js代碼部分。
js代碼可大致分成三部分:繪製棋盤。繪製棋子,判斷勝負
繪製棋盤:
這是固定操作,直接看代碼。
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();/* 將之前所有的路徑全部繪製一次 */
繪製棋子:
這裏除了繪製黑白棋子外,還需要繪製棋盤中五個小黑點,具體操作與繪製棋盤差不多。
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();
}
判斷勝負:
理論上在棋盤的任意位置只要構成上下左右和四個斜對角總共八個方向有連續的五個同樣顏色的棋子即可判勝,但是這裏我採取進一步優化的算法思想,原方法需要遍歷整個棋盤,時間複雜度爲O(n^2),我採用O(1)的時間複雜度算法。按照我們知道的規則,一旦判斷出勝負,此局就結束,所以我們只需要判斷最新添加的那顆棋子是否構成勝負即可!
var mode = [
[1, 0],
[0, 1],
[1, 1],
[1, -1]
]
function judge(x, y, color, mode) {
var count = 1;
for (var i = 1; i < 5; 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 < 5; i++) {
if (mapChess[x - i * mode[0]]) {
if (mapChess[x - i * mode[0]][y - i * mode[1]] == color) {
count++;
} else {
break;
}
}
}
return count >= 5 ? true : false;
}
注:原本是需要用八個循環來判斷八個方向,但是這個會使得代碼顯得多而冗餘,所以這裏巧借一個二維數組來使得代碼更加精煉!
最後我們需要一個start函數來聚合之前所有的函數(爲了便於理解,我把完整的js代碼都附上)
<script>
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();
var musicPromise = 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]) ||
judge(dx, dy, color, mode[1]) ||
judge(dx, dy, color, mode[2]) ||
judge(dx, dy, color, mode[3])
) {
music.muted = true;
step % 2 == 0 ? alert("黑棋獲勝") : alert("白棋獲勝");
canvas.removeEventListener('click', start, false);
return;
}
if (audioPromise !== undefined) {
audioPromise.then(_ => {
audio.paused = true;
})
.catch(error => {
});
}
step++;
}
}
function judge(x, y, color, mode) {
var count = 1;
for (var i = 1; i < 5; 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 < 5; i++) {
if (mapChess[x - i * mode[0]]) {
if (mapChess[x - i * mode[0]][y - i * mode[1]] == color) {
count++;
} else {
break;
}
}
}
return count >= 5 ? true : false;
}
</script>
<script>
var canvas = document.querySelector("canvas");
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);
canvas.addEventListener('click', start, false);
</script>
對於綁定的start事件我再補充兩點:
1.之所以把start不設置成匿名函數,因爲匿名函數無法解綁,在效果上也就是遊戲結束了還能繼續下棋,這不符合我們認知的規則。
2.在音效處理上(這裏Chrome有毒),由於Chrome不支持autoplay方法,所以我才使用了手動play,但是我最後部署到阿里雲後連play都不支持了,真心難受。
https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
這個鏈接給出了好幾種解決有關autoplay問題的方法。
最後可以通此鏈接查看效果(最終版):前端五子棋
至於五子棋AI部分,我個人也很喜歡五子棋ai算法,但是這部分的確很難,來日方長。