基于微信小程序的五子棋小程序(含简单人机)
- 运行截图
- 项目结构目录
- 基本思路
- 实现过程
- 棋盘的生成
- 落子
- 判断胜负
- 悔棋
- 人机对战的实现
- 权值表
- 机器人落子逻辑
- 改进胜负判断方法
- 更多功能
- 结语
运行截图
(界面比较丑,凑合看就行)
这是人机对战界面
这是双人对战界面(此处的双人对战指两人用同一手机轮流点击落子)
项目结构目录
本文重点在于人机对战模块(Player_VS_AI)和人人对战模块(Player_VS_Player)的实现,所以菜单模块之类的就省略了。
基本思路
先考虑双人对战的实现思路,基本逻辑为:
· 黑方点击屏幕,反馈相应坐标
· 在对应坐标上显示黑子
· 对这个黑子判断其是否造成五子相连
· 如果没有造成五子相连,则游戏继续
· 白方点击屏幕,反馈坐标
· 在对应坐标显示白子
· 对这个白子判断其是否造成五子相连
· 如果没有造成五子相连,则游戏继续
以此不断循环
然后是人机对战的实现思路,其实就是把白方点击屏幕确定坐标的操作改为电脑通过一系列计算获得坐标。这个项目中的落子坐标算法较为简单,但是效果还算不错,这个在后文进行详细讲解。
实现过程
首先我们需要实现一些基本功能
棋盘的生成
首先我们需要生成棋盘
这里我使用了wx:for循环生成了15*15共225个小方格
<!--index.wxml-->
<view class="bg">
<view class="container_1">
<view class="chessBoard">
<view class="block" wx:for="{{chessBoard}}" wx:key="{{index}}" bindtap="step" data-pos="{{index}}">
<view class="{{item}}"></view>
</view>
</view>
</view>
<button bindtap="restart" type="warning" class="restart-but">重新开始</button>
<button bindtap="regretChess" type="warning" class="restart-but">悔棋</button>
<button bindtap="getBackToMenu" type="warning" class="restart-but">返回</button>
<text>developed by zhx</text>
</view>
//棋盘数组初始化
function initChessBoard() {
let arr = [];
//选择使用一维数组来存储棋子,因为使用二维数组较为复杂
for (let i = 0; i < 225; i++) {
arr.push('0')
}
return arr;
}
var Pi = Page({
data: {
logs: [],
chessBoard: initChessBoard(), //棋盘
result: "",
count: 0,
},
directions: [
[1, 0],
[0, 1],
[1, 1],
[-1, 1]
],
这里使用一个一维数组chessBoard储存225个位置的棋子情况,0为空,white为白子,black为黑子。
生成的小格子的样式在正常运行时应该设置为透明的,为了清楚地向读者展示小格子的生成与排布情况, 我暂时将小格子设置为半透明,如下图
可以看到,每个格子都位于棋盘的交叉点(棋盘是背景图,这一步需要通过不断调整大小与位置来实现),而棋子其实是格子内部的不同显示样式。这部分的wxss代码如下,block是透明小方格,white是白棋,black是黑棋。
.block{
width:6%;
height:6%;
background-color:rgba(0, 0, 0, 0);
margin-left:0.6%;
margin-top:0.6%;
float:left;
}
.white{
width:100%;
height:100%;
border-radius:50%;
background-color:#ffffff;
box-shadow: gray 3px 3px 5px;
}
.black{
width:100%;
height:100%;
border-radius:50%;
background-color:#000000;
box-shadow: gray 3px 3px 5px;
}
落子
每个棋盘小格子都有点击事件step,用于落子操作
点击后,先判断对应位置是否已经有棋子
如果没有,则落入相应颜色的子(写入chessBoard数组)
js代码为:
//落子
step(event) {
var pos = event.currentTarget.dataset.pos;
if (this.data.chessBoard[pos] == "white" || this.data.chessBoard[pos] == "black") return;
this.data.count++;
if (this.data.count % 2) //这里认为1代表黑棋,2代表白棋
this.data.chessBoard[pos] = "black";
else
this.data.chessBoard[pos] = "white";
stack.push(pos) //压入栈中,用于处理悔棋
this.setData({ //将逻辑层数据传到视图层
chessBoard: this.data.chessBoard
})
this.judge(pos);
},
判断胜负
具体实现看以下代码及注释即可
其实这里使用一维数组来判断是有bug的,会导致把最左边一个与上一行的最右边一格认为是在同一行。要改进也很简单,就是把一维数组转化为二维数组。(改进可以参考后面人机对战的胜负判断部分,这里我懒得改了hhhh)
//判断胜负
judge(pos) {
var color = this.data.chessBoard[pos];
var x0 = parseInt(pos / 15), //第几列
y0 = pos % 15, x, y, round; //第几行
//以x0*15+y0便可得到棋子的下标
for (var i = 0; i < 4; i++) { //统计四个方向
var number = 0;
//正向先检测五个
round = 0;
for (x = x0, y = y0; round < 5; x += this.directions[i][0], y += this.directions[i][1], round++) {
if (this.data.chessBoard[15 * x + y] == color) {
number++;
}
else {
break;
}
}
//相反方向再检测五个
round = 0;
for (x = x0, y = y0; round < 5; x -= this.directions[i][0], y -= this.directions[i][1], round++) {
if (this.data.chessBoard[15 * x + y] == color) {
number++;
}
else {
break;
}
}
//检测到五子连珠
if (number >= 6) { //6个是因为落子被统计了两次
if(color=="black")
var t_color="黑方"
else
var t_color="白方"
//跳出对话框
wx.showModal({
title: t_color + '获胜!',
content: '再来一局?',
success: function (res) {
if (res.confirm) {
wx.navigateTo({
url: "./Player_VS_Player"
});
}
else if(res.cancel){
wx.navigateTo({
url: '../Menu/Menu',
})
}
},
})
}
}
},
悔棋
然后是悔棋模块,简单地利用一下栈即可。
(入栈操作在落子函数中)
//悔棋
regretChess(){
if(stack.length!=0){
//从栈中删除两个元素
var returnChessPos=stack.pop();
this.data.chessBoard[returnChessPos]="0";
this.data.count--;
//将逻辑层数据传到视图层
this.setData({
chessBoard: this.data.chessBoard
})
}
else{
return;
}
},
至此,一个简单的双人对战五子棋已经完成了,就这么简单。
人机对战的实现
人机对战中的电脑应该如何落子?这里我选择了最简单的权值法,即赋予每种棋子排列方式不同的权值,每次电脑落子时,先测算所有可落子点的权值,在权值最大的点进行落子。
至于权值表里要设置哪些排列以及权值如何设置,可以参考我下面这个表格,数据经过测试与微调得来。(不建议照搬,因为最终的权值算法可能与我的不同,结果也会不同,需要自己慢慢测试调整)
权值表
以1代表黑棋,2代表白棋。例如:112即代表“黑黑白”排列。由于玩家总是黑棋,所以认为黑棋是敌方棋子。
排列 | Value |
1 | 20 |
11 | 410 |
111 | 500 |
1111 | 8000 |
12 | 4 |
112 | 70 |
1112 | 450 |
11112 | 8000 |
2 | 8 |
22 | 80 |
222 | 470 |
2222 | 9000 |
22221 | 10000 |
21 | 6 |
221 | 60 |
2221 | 600 |
121 | 5 |
1221 | 5 |
2112 | 5 |
212 | 5 |
机器人落子逻辑
为了便于操作以及修正人人对战中胜负判断的BUG,我们先把一维棋子数组转化为二维的
//初始化二维数组
for(var i=0;i<15;i++){
this.data.chessBoard_2d.push([])
for(var j=0;j<15;j++){
this.data.chessBoard_2d[i].push('0')
}
}
//落子
step(event) {
var pos = event.currentTarget.dataset.pos;
console.log(pos)
if (this.data.chessBoard[pos] == "white" || this.data.chessBoard[pos] == "black") return;
this.data.chessBoard[pos] = "black";
stack.push(pos) //压入栈中
this.setData({ //将逻辑层数据传到视图层
chessBoard: this.data.chessBoard
})
//将棋盘转化为二维数组,便于胜负判定与AI计算
for(var i=0;i<15;i++){
for(var j=0;j<15;j++){
this.data.chessBoard_2d[i][j]=this.data.chessBoard[i*15+j];
}
}
this.judge(pos);
// console.log(this.data.chessBoard_2d)
if(win==0){
this.AI_step()
//机器人落子跟在玩家之后
}
},
每当玩家落子后,判断胜负,然后紧接着执行机器人落子AI_step():
//AI落子
AI_step(){
for(let i=0;i<225;i++){
this.data.chessScore[i]=0; //权值清零
}
//遍历棋盘的所有位置
for(let i=0;i<225;i++){
if(this.data.chessBoard[i]=='0'){ //选出空点进行权值计算
this.calculatePositionWeight_white(i);
}
}
//选出权值最大的落子点
var max,maxPos=0;
// console.log(this.data.chessScore)
for(let i=1,max=this.data.chessScore[0];i<225;i++){
if(max<this.data.chessScore[i]){
max=this.data.chessScore[i];
maxPos=i;
}
}
// console.log(max)
//落子
this.data.chessBoard[maxPos]="white";
stack.push(maxPos) //压入栈中
//将棋盘转化为二维数组,便于胜负判定与AI计算
for(var i=0;i<15;i++){
for(var j=0;j<15;j++){
this.data.chessBoard_2d[i][j]=this.data.chessBoard[i*15+j];
}
}
this.judge(maxPos)
this.setData({ //将逻辑层数据传到视图层
chessBoard: this.data.chessBoard
})
console.log('AI:'+maxPos)
},
其中chessScore数组储存每个空点的权值。那么这里最关键的部分就是这个chessScore数组中各个点的权值的计算方法。
改进胜负判断方法
在分析上面这个问题之前,我们先把之前人人对战里的胜负判断BUG解决一下,其实就是把一维数组改为二维数组再进行判断。
//判断胜负
judge(pos) {
var color = this.data.chessBoard[pos];
var y0 = parseInt(pos / 15), //第几行
x0 = pos % 15, x, y, round; //第几列
//以y0*15+x0便可得到棋子的下标
for (var i = 0; i < 4; i++) { //统计四个方向
var number = 0;
//正向先检测五个
round = 0;
for (x = x0, y = y0; round < 5; x += this.directions[i][0], y += this.directions[i][1], round++) {
if (x>=0&&x<=14&&y>=0&&y<=14&&this.data.chessBoard_2d[y][x] == color) {
number++;
}
else {
break;
}
}
//相反方向再检测五个
round = 0;
for (x = x0, y = y0; round < 5; x -= this.directions[i][0], y -= this.directions[i][1], round++) {
if (x>=0&&x<=14&&y>=0&&y<=14&&this.data.chessBoard_2d[y][x] == color) {
number++;
}
else {
break;
}
}
//检测到五子连珠
if (number >= 6) { //6个是因为落子棋子被统计了两次
win=1;
if(color=="black")
var t_color="黑方"
else
var t_color="白方"
//跳出对话框
wx.showModal({
title: t_color + '获胜!',
content: '再来一局?',
success: function (res) {
if (res.confirm) {
wx.navigateTo({
url: "./Player_VS_AI"
});
}
else{
win=0;
}
},
})
}
}
},
回到权值计算方法,我利用了calculatePositionWeight_white(i)函数,对于每个输入的空点坐标,这个函数都计算并返回了它的权值。(函数后面的“_white”表示这个是当机器人执白子时使用的函数,如果要对游戏功能进行扩展,比如加入提示功能,机器人就会执黑子,算法就需要做一些微调)
下面是这个函数的实现过程:
//计算目标点的权值(白棋ai使用)
calculatePositionWeight_white(position){
var y0 = parseInt(position / 15), //第几行
x0 = position % 15, //第几列
maxWeight=0;
//计算目标点权值
var right=this.countChessWeightInOneDirection_white(x0,y0,1,0)
var left=this.countChessWeightInOneDirection_white(x0,y0,-1,0)
var down=this.countChessWeightInOneDirection_white(x0,y0,0,1)
var up=this.countChessWeightInOneDirection_white(x0,y0,0,-1)
var right_up=this.countChessWeightInOneDirection_white(x0,y0,1,-1)
var left_up=this.countChessWeightInOneDirection_white(x0,y0,-1,-1)
var left_down=this.countChessWeightInOneDirection_white(x0,y0,-1,1)
var right_down=this.countChessWeightInOneDirection_white(x0,y0,1,1)
var weightArr=[right,left,down,up,right_up,left_down,left_up,right_down]
//这里对于权值的计算可以优化,从而解决双活三的类似问题
//原始版本:仅把目标点八个方向上的权值最大的布局作为目标点的权值
//效果:无法处理‘11-11’型问题,容易被下套
// for(var i=0;i<8;i++){
// if(maxWeight<=weightArr[i]){
// maxWeight=weightArr[i]
// }
// }
//处理方法一:将目标点两端的布局权重相加再求最大值
//效果:可以解决一些类似双活三的问题,但是存在部分落子不正常的情况,这里必须要根据实际情况对权值进行一些修改,处理一些比较明显的问题之后效果就得到很大改善,目前对于隔子下套已经可以较好应对,但是双活三问题依旧存在
for(var i=0;i<4;i++){
if(maxWeight<=(weightArr[2*i]+weightArr[2*i+1])){
maxWeight=(weightArr[2*i]+weightArr[2*i+1])
}
}
this.data.chessScore[position]=maxWeight
},
其中countChessWeightInOneDirection_white函数如下。weightDic是权值表。
这个函数先获取空点的某个方向上的相连的棋子的布局,如“空黑黑白白白”则获得shape的值为11222,但是如果中间不相连则指获得相连部分的布局,如“空黑黑空白白”则只获得前两个黑子的布局,shape即为11.
然后再将获得的shape与权值表比对,获得权值。这里的权值只是这一个方向上的权值,所以为了对所有方向都进行扫描,我们需要改变方向并执行八次。
//计算某一方向上的棋子情况,必须与目标点相邻,返回权值(白棋ai使用)
countChessWeightInOneDirection_white(x,y,direction_x,direction_y){
this.data.count=0
this.data.shape=''
while(this.data.count<=4){
x+=direction_x;y+=direction_y;this.data.count++;
if(x>=0&&x<=14&&y>=0&&y<=14&&this.data.chessBoard_2d[y][x]=='black'){
this.data.shape+='1'
}
else if(x>=0&&x<=14&&y>=0&&y<=14&&this.data.chessBoard_2d[y][x]=='white'){
this.data.shape+='2'
}
else {
break
}
}
while(1){
var i;
for(i=0;i<40;i++){
if(this.data.shape==weightDic[i]){
return weightDic[i+1]
}
}
return 0;
}
},
当我们获得了一个空点的周围八个方向的权值,那我们如何计算这个点的最终权值呢?最初我的办法是将八个权值的最大值作为这个点的权值,但是这种方法有个比较严重的问题,就是当存在形如“黑黑空黑黑”时,中间这个点白方必须落子,但这种方法获得的这个点的权值只有410,也就是说如果这个棋盘上黑方在某处还有一个“空黑黑黑空”,对应权值为500,白棋机器人就会把棋子落在这三个黑子旁边,那么这样的机器人就宛如人工智障显得很傻。
我的改进方法其实很简单,就是把相反方向的两个权值两两相加,在把这四对想法方向的权值和的最大值作为这个点的权值,这样就较好地解决了“被下套”的问题。
更多功能
我们不妨利用这个机器人算法给自己添加一个“提示”功能,让机器人帮你(黑棋)落子。(这样就实现了左右互搏)
只要在扫描获取shape值时,把黑棋当成2,白棋当成1即可,这里就不多啰嗦了,可以看一下左右互搏的结果:
可以看到左右互搏还是能跑好一会儿的
结语
如果这篇文章拯救了你的课设,请点个赞再走好吗~球球了!
有问题欢迎在评论区交流,gitee私信不常看
项目文件已上传码云
https://gitee.com/zhang-haoxin/wechat_chess