五子棋人机博弈游戏(cocos creator)

参考文章:【Cocos Creator 实战教程(1)】——人机对战五子棋(节点事件相关)
源码:goBang

五子棋是起源于中国古代的传统黑白棋种之一。现代五 子棋日文称之为“连珠”,英译为“Renju”,英文称之为 “Gobang”或“FIR”(Five in a Row 的缩写),亦有“连五子”、 “五子连”、“串珠”、“五目”、“五目碰”等多种称谓。

思考一:作为对手的系统用什么算法下棋?

估值函数、搜索算法和胜负判断等

博弈算法,在极大极小值搜索中应用alpha-beta剪枝

智能五子棋博弈程序的核心算法

智能五子棋中的算法研究

人机版五子棋两种算法概述

思考二:人机博弈的要点

1.棋局的状态能够在机器中表示出来,并能让程序知道当时的博弈状态

2.合法的走法规则如何在机器中实现,以便不让机器随便乱走而有失公平

3.如何让机器从所有的合法走法中选择最佳的走法

4.一种判断博弈状态优劣的方法,并能让机器能够做出智能的选择

5.一个显示博弈状态的界面,有了这样的界面程序才能用的起来而有意义

思考三:五子棋下棋规矩

作为了解,新手的我没做

五子棋对局,执行黑方指定开局、三手可交换、五手两打的规定。

整个对局过程中黑方有禁手,白方无禁手。

黑方禁手有三三禁手、四四禁手和长连禁手三种

思考四:人机下棋逻辑

系统先下,黑棋落子,交换下子顺序

玩家下,监测胜负(无胜负,交换下子顺序)

系统下(五元组中找最优位置),监测胜负(无胜负,交换下子顺序)

。。。

直到分出胜负(这里未考虑平局)

出现提示窗,告知玩家战局结果,同时可选择“返回菜单”或“再来一局”

具体实现:涉及知识点

1. 预制棋子资源

官方文档--预制资源

press.png

将其改名为Chess拖入下面assets文件夹使其成为预制资源

2. 按钮添加事件

1.在canvas节点上挂载Menu脚本组件

node.png

2.在按钮事件中,拖拽和选择相应的Target,Component和Handler

buttonEvent.png

Button事件

属性 功能说明
Target 带有脚本组件的节点
Component 脚本组件名称
Handler 指定一个回调函数,当用户点击 Button 并释放时会触发此函数
CustomEventData 用户指定任意的字符串作为事件回调的最后一个参数传入

3. 添加用户脚本组件

user.png

4.初始化棋盘225个棋子节点

  • 计算使每个棋子节点位于指定位置
cc.Class({
    extends: cc.Component,
    properties: {
        chessPrefab:{//棋子的预制资源
            default:null,
            type:cc.Prefab
        },   
        chessList:{//棋子节点的集合,用一维数组表示二维位置
            default: [],
            type: [cc.node]
        },
        whiteSpriteFrame:{//白棋的图片
            default:null,
            type:cc.SpriteFrame
        },
        
        blackSpriteFrame:{//黑棋的图片
            default:null,
            type:cc.SpriteFrame
        },
        
        touchChess:{//每一回合落下的棋子
            default:null,
            type:cc.Node,
            visible:false//属性窗口不显示
        }
    },
    onLoad: function(){
        var self = this;
        for(var y = 0; y < 15; y++){
            for(var x = 0; x < 15; x++){
                var newNode = cc.instantiate(this.chessPrefab); //复制Chess预制资源
                this.node.addChild(newNode);
                //根据棋盘和棋子大小计算使每个棋子节点位于指定位置
                newNode.setPosition(cc.p(x*40+20,y*40+20));
                newNode.tag = y*15+x;//根据每个节点的tag就可以算出其二维坐标
                this.chessList.push(newNode);
            }
        }
    }
})

初始化棋子节点断点截图


console1.png
  • 为每个棋子节点添加事件
//此代码包含在onLoad for循环内,方便解读,提出来
//玩家点击棋子节点触发以下代码
newNode.on(cc.Node.EventType.TOUCH_END,function(event){
    self.touchChess = this;
    //下子顺序到白棋
    if(self.gameState ===  'white' && this.getComponent(cc.Sprite).spriteFrame === null){
        //下子后添加棋子图片使棋子显示
        this.getComponent(cc.Sprite).spriteFrame = self.whiteSpriteFrame;
        //监测胜负
        self.judgeOver();
        //轮到电脑下棋
        if(self.gameState == 'black'){
            self.scheduleOnce(function(){self.ai()},1);//延迟一秒电脑下棋
        }
    }
});

5.添加五元数组(横,竖,右上斜,右下斜),572个

//横向
for(var y=0;y<15;y++){
    for(var x=0;x<11;x++){
        this.fiveGroup.push([y*15+x,y*15+x+1,y*15+x+2,y*15+x+3,y*15+x+4]);
    }  
}
//纵向
for(var x=0;x<15;x++){
    for(var y=0;y<11;y++){
        this.fiveGroup.push([y*15+x,(y+1)*15+x,(y+2)*15+x,(y+3)*15+x,(y+4)*15+x]);
    }
}
//右上斜向
for(var b=-10;b<=10;b++){
    for(var x=0;x<11;x++){
        if(b+x<0||b+x>10){
            continue;
        }else{
            this.fiveGroup.push([(b+x)*15+x,(b+x+1)*15+x+1,(b+x+2)*15+x+2,(b+x+3)*15+x+3,(b+x+4)*15+x+4]);
        }
    }
}
//右下斜向
for(var b=4;b<=24;b++){
    for(var y=0;y<11;y++){
        if(b-y<4||b-y>14){
            continue;
        }else{
            this.fiveGroup.push([y*15+b-y,(y+1)*15+b-y-1,(y+2)*15+b-y-2,(y+3)*15+b-y-3,(y+4)*15+b-y-4]);
        }
    }
}

6.系统在最高分的五元组中找到最优位置落子

系统为黑棋的评分表:

//五元组中无子
GGTupleTypeBlank = 7,
//五元组中包含一个黑子
GGTupleTypeB = 35,
//五元组中包含两个黑子
GGTupleTypeBB = 800,
//五元组中包含三个黑子
GGTupleTypeBBB = 15000,
//五元组中包含四个黑子
GGTupleTypeBBBB = 800000,
//五元组中包含一个白子
GGTupleTypeW = 15,
//五元组中包含两个白子
GGTupleTypeWW = 400,
//五元组中包含三个白子
GGTupleTypeWWW = 1800,
//五元组中包含四个白子
GGTupleTypeWWWW = 100000,
//五元组中包含黑白子都有,此五元组为无效的,评分为零
GGTupleTypePolluted = 0

//评分
for(var i=0;i<this.fiveGroup.length;i++){
    var b=0;//五元组里黑棋的个数
    var w=0;//五元组里白棋的个数
    for(var j=0;j<5;j++){
        if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.blackSpriteFrame){
            b++;
        }else if(this.chessList[this.fiveGroup[i]         [j]].getComponent(cc.Sprite).spriteFrame == this.whiteSpriteFrame){
            w++;
        }
    }

    if(b+w==0){
        this.fiveGroupScore[i] = 7;
    }else if(b>0&&w>0){
        this.fiveGroupScore[i] = 0;
    }else if(b==0&&w==1){
        this.fiveGroupScore[i] = 15;
    }else if(b==0&&w==2){
        this.fiveGroupScore[i] = 400;
    }else if(b==0&&w==3){
        this.fiveGroupScore[i] = 1800;
    }else if(b==0&&w==4){
        this.fiveGroupScore[i] = 100000;
    }else if(w==0&&b==1){
        this.fiveGroupScore[i] = 35;
    }else if(w==0&&b==2){
        this.fiveGroupScore[i] = 800;
    }else if(w==0&&b==3){
        this.fiveGroupScore[i] = 15000;
    }else if(w==0&&b==4){
        this.fiveGroupScore[i] = 800000;
    }            
}

​ 找最优位置下子

//找最高分的五元组
var hScore=0;
var mPosition=0;
for(var i=0;i<this.fiveGroupScore.length;i++){
    if(this.fiveGroupScore[i]>hScore){
        hScore = this.fiveGroupScore[i];
        mPosition = (function(x){//js闭包
        return x;
        })(i);
    }
}
//在最高分的五元组里找到最优下子位置(这段我没明白啥逻辑,for循环中只保留第一个判断也可以)
var flag1 = false;//无子
var flag2 = false;//有子
var nPosition = 0;
for(var i=0;i<5;i++){
    if(!flag1&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){
        nPosition = (function(x){return x})(i);
    }
    if(!flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame != null){
        flag1 = true;
        flag2 = true;
    }
    if(flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){
        nPosition = (function(x){return x})(i);
        break;
    }
}
//在最最优位置下子
this.chessList[this.fiveGroup[mPosition][nPosition]].getComponent(cc.Sprite).spriteFrame = this.blackSpriteFrame;
this.touchChess = this.chessList[this.fiveGroup[mPosition][nPosition]];
  1. 最优位置断点
bestPosition1.png
  1. 最优位置截图
bestPosition2.png

7.监测胜负

judgeOver:function(){
    var x0 = this.touchChess.tag % 15;
    var y0 = parseInt(this.touchChess.tag / 15);
    //判断横向
    var fiveCount = 0;
    for(var x = 0;x < 15;x++){
        if((this.chessList[y0*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){
            fiveCount++; 
            if(fiveCount==5){
                if(this.gameState === 'white'){
                    this.overLabel.string = "你赢了";
                    this.overSprite.node.x = 0;
                }else{
                    this.overLabel.string = "你输了";
                    this.overSprite.node.x = 0;
                }
                this.gameState = 'over';
                return;
            }
        }else{
            fiveCount=0;
        }
    }
    //判断纵向
    fiveCount = 0;
    for(var y = 0;y < 15;y++){
        if((this.chessList[y*15+x0].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){
            fiveCount++; 
            if(fiveCount==5){
                if(this.gameState === 'white'){
                    this.overLabel.string = "你赢了";
                    this.overSprite.node.x = 0;
                }else{
                    this.overLabel.string = "你输了";
                    this.overSprite.node.x = 0;
                }
                this.gameState = 'over';
                return;
            }
        }else{
            fiveCount=0;
        }
    }
    //判断右上斜向
    var f = y0 - x0;
    fiveCount = 0;
    for(var x = 0;x < 15;x++){
        if(f+x < 0 || f+x > 14){
            continue;
        }
        if((this.chessList[(f+x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){
            fiveCount++; 
            if(fiveCount==5){
                if(this.gameState === 'white'){
                    this.overLabel.string = "你赢了";
                    this.overSprite.node.x = 0;
                }else{
                    this.overLabel.string = "你输了";
                    this.overSprite.node.x = 0;
                }
                this.gameState = 'over';
                return;
            }
        }else{
            fiveCount=0;
        }
    }
    //判断右下斜向
    f = y0 + x0;
    fiveCount = 0;
    for(var x = 0;x < 15;x++){
        if(f-x < 0 || f-x > 14){
            continue;
        }
        if((this.chessList[(f-x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){
            fiveCount++; 
            if(fiveCount==5){
                if(this.gameState === 'white'){
                    this.overLabel.string = "你赢了";
                    this.overSprite.node.x = 0;
                }else{
                    this.overLabel.string = "你输了";
                    this.overSprite.node.x = 0;
                }
                this.gameState = 'over';
                return;
            }
        }else{
            fiveCount=0;
        }
    }
    //没有输赢交换下子顺序
    if(this.gameState === 'black'){
        this.gameState = 'white';
    }else{
        this.gameState = 'black';
    }
}

个人想法

  1. 开局后,隔一会儿(如1分钟),玩家未落子,出提示窗,确认是否继续玩

  2. 设置积分,奖励制度

这是我学习五子棋游戏开发的记录,后续还会写其他游戏开发,加油!

推荐阅读更多精彩内容