试玩链接:五子棋
效果图如下:
与上个版本相比,主要实现了两个功能:
一个是处理的音效消失的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,但是就单步下棋水平而言,我个人感觉已经马马虎虎了。后续的重点将是给与电脑算棋甚至是算杀的功能。这部分算法的难度很大,我目前暂时还没有这个能力,所以还得更加持续学习,我目前有个思路就是回溯剪枝算法。这里我很欢迎大佬们评论区留言赐教!
来源:CSDN
作者:奇喑
链接:https://blog.csdn.net/asd0356/article/details/104750446