五子棋程序设计实现技术文档

五子棋程序设计实现文档


前言

博弈算法的设计。通过五子棋,国际象棋,跳棋等博弈游戏的软件设计,巩固本课程所学的知识,包括状态空间搜索方法、与或树的一般搜索方法、博弈树的搜索及α-β剪枝技术等。

一、运行截图

双人对战界面(此处的双人对战指两人用同一手机轮流点击落子)
在这里插入图片描述

人机对战界面
在这里插入图片描述
主页面:
在这里插入图片描述
在这里插入图片描述
项目结构目录
在这里插入图片描述

二、基本思路

先考虑双人对战的实现思路,基本逻辑为:
· 黑方点击屏幕,反馈相应坐标
· 在对应坐标上显示黑子
· 对这个黑子判断其是否造成五子相连
· 如果没有造成五子相连,则游戏继续
· 白方点击屏幕,反馈坐标
· 在对应坐标显示白子
· 对这个白子判断其是否造成五子相连
· 如果没有造成五子相连,则游戏继续
以此不断循环

1.实现过程

首先我们需要实现一些基本功能
棋盘的生成
首先我们需要生成棋盘

这里我使用了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;
}

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);
  },

2.落子

每个棋盘小格子都有点击事件step,用于落子操作
点击后,先判断对应位置是否已经有棋子
如果没有,则落入相应颜色的子(写入chessBoard数组)

判断胜负
具体实现看以下代码及注释即可

其实这里使用一维数组来判断是有bug的,会导致把最左边一个与上一行的最右边一格认为是在同一行。要改进也很简单,就是把一维数组转化为二维数组。

  
  //判断胜负
  judge(pos) {
    var that = this;
    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) {
              that.goplaygame ();
            }
            else if(res.cancel){
              wx.navigateTo({
                url: '../Menu/Menu',
              })
            }
          },
        })
      }
    }
  },

3.悔棋

然后是悔棋模块,简单地利用一下栈即可。
(入栈操作在落子函数中)这里我定义了黑方白方都只有5次的悔棋机会。

  data: {
    logs: [],
    chessBoard: initChessBoard(),   //棋盘
    result: "",
    count: 0,
    whitenum:5,//白棋悔棋次数
    blacknum:5 //黑棋悔棋次数
  },
  //悔棋
  regretChess(){
    var getnum =0
    if(stack.length!=0){
      //从栈中删除两个元素
      //判断是黑棋悔棋还是白棋悔棋
      if (this.data.count % 2) {
          //判断当前是否还有机会悔棋
          if(this.data.blacknum!=0){
            var returnChessPos=stack.pop();
            this.data.chessBoard[returnChessPos]="0";
            getnum=this.data.blacknum-1;
            this.setData({    
              chessBoard: this.data.chessBoard,
              blacknum:getnum  
            })
            this.data.count--;
          }else{
            wx.showModal({
              content: '你已经没有悔棋机会',
            })
          }
      }else{
        if(this.data.whitenum!=0){
          var returnChessPos=stack.pop();
          this.data.chessBoard[returnChessPos]="0";
          getnum=this.data.whitenum-1;
          this.setData({    
            chessBoard: this.data.chessBoard,
            whitenum:getnum  
          })
          this.data.count--;
        }else{
          wx.showModal({
            content: '你已经没有悔棋机会',
          })
        }
      } 
      //将逻辑层数据传到视图层
    }
    else{
      wx.showModal({
        content: '当前没有落子',
      })
      return;
    }
  },

4.人机对战的实现

人机对战中的电脑应该如何落子?这里我选择了最简单的权值法,即赋予每种棋子排列方式不同的权值,每次电脑落子时,先测算所有可落子点的权值,在权值最大的点进行落子。

至于权值表里要设置哪些排列以及权值如何设置,可以参考我下面这个表格,数据经过测试与微调得来。

权值表
以1代表黑棋,2代表白棋。例如:112即代表“黑黑白”排列。由于玩家总是黑棋,所以认为黑棋是敌方棋子。
在这里插入图片描述

var weightDic=[   //权值表,默认1黑2白,共20种情况
  '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,
];

1.机器人落子逻辑**

为了便于操作以及修正人人对战中胜负判断的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数组中各个点的权值的计算方法。

2.改进胜负判断方法

在分析上面这个问题之前,我们先把之前人人对战里的胜负判断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”表示这个是当机器人执白子时使用的函数,如果要对游戏功能进行扩展,比如加入提示功能,机器人就会执黑子,算法就需要做一些微调)
下面是这个函数的实现过程:

3.计算目标点的权值(白棋ai使用)

 //计算目标点的权值(白棋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

  },

4.计算目标点的权值(黑棋ai使用)**

  //计算目标点的权值(黑棋ai使用)
  calculatePositionWeight_black(position){
    var y0 = parseInt(position / 15),  //第几行
    x0 = position % 15,   //第几列
    maxWeight=0;
    
    //计算目标点权值
    var right=this.countChessWeightInOneDirection_black(x0,y0,1,0)
    var left=this.countChessWeightInOneDirection_black(x0,y0,-1,0)
    var down=this.countChessWeightInOneDirection_black(x0,y0,0,1)
    var up=this.countChessWeightInOneDirection_black(x0,y0,0,-1)
    var right_up=this.countChessWeightInOneDirection_black(x0,y0,1,-1)
    var left_up=this.countChessWeightInOneDirection_black(x0,y0,-1,-1)
    var left_down=this.countChessWeightInOneDirection_black(x0,y0,-1,1)
    var right_down=this.countChessWeightInOneDirection_black(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,白棋机器人就会把棋子落在这三个黑子旁边,那么这样的机器人就宛如人工智障显得很傻。

我的改进方法其实很简单,就是把相反方向的两个权值两两相加,在把这四对想法方向的权值和的最大值作为这个点的权值,这样就较好地解决了“被下套”的问题。

4.扩展功能

1.智能“提示”功能

我利用这个机器人算法给自己添加一个“提示”功能,让机器人帮你(黑棋)落子。(这样就实现了左右互搏)
只要在扫描获取shape值时,把黑棋当成2,白棋当成1即可
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默着忍受

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值