之前用后端语言实现了一个控制台应用的简化五子棋游戏,效果总是不太好,不能用鼠标操作是硬伤,今儿我们就用最基础的前端语言来实现一个五子棋游戏。先看看实现后的页面效果:
看到页面显示不难分析出:
浏览器页面只有中间一块棋盘区域,很简单,也很单调。但是这个棋盘的绘制很难用一般的前端技术实现,所以这里我使用专门的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算法,但是这部分的确很难,来日方长。