欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网页版井字游戏(TicTacToe)人机对战的制作(附思路和源码)

程序员文章站 2022-05-19 15:42:18
...

井字游戏的规则是:在一个井字格子的棋盘里下棋,横竖斜一旦三子连子,则胜。而事实上,遵循一定的规则,该游戏便能保证不败,即至少是平局。
若是两人对战,则仅需要判断“胜负平”三种状态即可,比较简单,而人机对战的难点便在于让机器立于不败之地的下棋规则。下面会重点讲解不败的思路。

先放一张游戏截图,程序演示与源码下载可以去:戳我演示或下载代码
网页版井字游戏(TicTacToe)人机对战的制作(附思路和源码)

在此先规定电脑一定是先手,如果电脑不是先手的话算法需要另外设计,但方法类似,在此暂不讨论。先说说电脑先手时保持不败的设计思路:
根据下图标出的编号来看:
网页版井字游戏(TicTacToe)人机对战的制作(附思路和源码)
前两子是有特别讲究的,不然可能会输。
第一子:只能在正中间或者四个角随机选一个,即1、3、5、7、9中任选一个。
第二子:分两种情况讨论。

  1. 若第一子在正中,即5号,那么如果玩家下在四个角(1、3、7、9),则第二子下其对角(9、7、3、1)。如果玩家下在某一行或某一列的中间格,即2、4、6、8的某一个,则第二子应下在靠近其的某个角。
    比如:

    电脑(第1子):5
    玩家(第1子):1
    电脑(第2子):9



    电脑(第1子):5
    玩家(第1子):2
    电脑(第2子):1或3(随机选择)

  2. 若第一子在四个角,即1、3、7、9其中之一,则也许根据玩家的下法来判断。
    若玩家下在正*,即5号格子,那么第二步就需下第一步的对角,即9、7、3、1。 如果玩家下的不是正*,那么第二步就下正*。 举例:

    电脑(第1子):3
    玩家(第1子):5
    电脑(第2子):7



    电脑(第1子):3
    玩家(第1子):9
    电脑(第2子):5

第二子以后,只需遵循一定规则即可。规则共三条,从第一条开始往下判断即可(注意一定要按顺序!!!)
1、判断电脑下某处后是否能直接胜出。即电脑下的子是否有二连珠且第三个位置还没有棋子。如果有,就往该处下棋,即可胜出,如果没有,往下。
2、判断玩家棋子是否有二连珠且对应连线的第三个位置为空位的,如果有,将该空位补上,如果没有,第三步。
3、遍历所有还没下棋的格子,对每一个格子进行判断,并统计出如果下这一格,能让几条线有二连珠且第三个位置为空位。选择统计的数量最多的一格下棋。有点抽象,拿下图举个例子,图中编号为下棋的顺序:
网页版井字游戏(TicTacToe)人机对战的制作(附思路和源码)
看第五步棋,也就是电脑的第三步,如果该棋下在第一行中间位置,能成线的条数为0,如果下在第一列中间位置,成线条数为1(第一列),而下在现在这个位置,成线条数为2(第一列与第三行)。而成线数越多,便越有优势,当成两条线时,便赢了。

下面放上源代码,核心代码有详细注释,可以直接去上面给的链接里切换编辑视图,点击右下角有地方下载。此处代码也可以直接复制即可运行:
html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>YinyouTicTacToe</title>
    <link href="https://fonts.googleapis.com/css?family=Itim" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
    <h1>Yinyou TicTacToe</h1>
    <div class="main">
            <div id="tic-1" class="tic">
                <span id="span-1" class="tic-span"></span>
            </div>
            <div id="tic-2" class="tic">
                <span id="span-2" class="tic-span"></span>
            </div>
            <div id="tic-3" class="tic">
                <span id="span-3" class="tic-span"></span>
            </div>
            <div id="tic-4" class="tic">
                <span id="span-4" class="tic-span"></span>
            </div>
            <div id="tic-5" class="tic">
                <span id="span-5" class="tic-span"></span>
            </div>
            <div id="tic-6" class="tic">
                <span id="span-6" class="tic-span"></span>
            </div>
            <div id="tic-7" class="tic">
                <span id="span-7" class="tic-span"></span>
            </div>
            <div id="tic-8" class="tic">
                <span id="span-8" class="tic-span"></span>
            </div>
            <div id="tic-9" class="tic">
                <span id="span-9" class="tic-span"></span>
            </div>  
    </div>

    <div class="back"></div>
    <div class="choose">
        <div class="choose-title">
            <h2>TicTacToe</h2>
            <hr>
            <p>Please choose whether you wanna use? × OR O</p>
        </div>
        <div class="choose-bt">
            <button id="cha">×</button>
            <button id="O">o</button>
        </div>
    </div>

    <div class="loser">
        Tie!
    </div>

    <script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
    <script type="text/javascript" src="js/tic.js"></script>
</body>
</html>

css

*{
    padding: 0;
    margin: 0;
    font-family: 'Itim', cursive;
}
html,body{
    background-color: #cb4042;
    width: 100%;
    height: 96%;
    background-position: fixed;
}

h1{
    text-align: center;
    color: #fff;
    font-size: 2em;
    margin-top: 3%;
}
.main{
    width: 486px;
    height: 486px;
    margin: auto;
    margin-top: 3%;
    background-color: #fff;
    text-align: center;
    z-index: 5;
}

.tic{
    display: inline-block;
    width: 160px;
    height: 160px;
    font-size: 1em;
    -webkit-text-size-adjust:100%;
    border:1px solid #cb4042;
    cursor: pointer;
    float: left;
    color: #000;
    position: relative;
}
.tic-span{
    font-size: 3em;
    color: #cb4042;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
}

.back{
    position: fixed;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: #000;
    opacity: .5;
    z-index: 20;
}

.choose{
    z-index: 21;
    position: absolute;
    top: 20%;
    left: 0;
    right: 0;
    margin: auto;
    width: 700px;
    color: #fff;
    background-color: #db4d6d;
    border-radius: 10px;
    outline: none;
    display: none;
}

.choose-title{
    padding-top: 20px;
    text-align: center;
}

.choose-bt{
    margin-top: 20px;
    margin-bottom: 15px;
    /* text-align: right;
    margin-right: 15px; */
    text-align: center;
}

.choose-bt button{
    font-family: sans-serif;
    width: 40px;
    height: 40px;
    font-size: 1.5em;
    text-align: center;
    background:#eee;
    border:1px solid #cb4042;
    border-radius: 50%;
    outline: none;
    color: #cb4042;
    margin-left: 50px;
    margin-right: 50px;
    cursor: pointer;
}

.choose-bt button:hover{
    background-color: #fff;
}

.loser{
    position: absolute;
    top: 0;
    height: 100%;
    width: 100%;
    text-align: center;
    font-size: 25em;
    z-index: 1;
    color: red;
    display: none;
}

js

$(document).ready(function(){
    var cmpt = "";  //电脑用的
    var user = "";  //用户用的
    var group = [0,0,0,0,0,0,0,0,0];    //记录九个棋格,0表示没下,1表示电脑,2表示玩家

    $(".choose").fadeIn(1000);
    $("#cha").on("click",function(){
        $(".choose").fadeOut(1);
        $(".back").fadeOut(1);
        cmpt = "O";
        user = "×";
        pcStep();
    });
    $("#O").on("click",function(){
        $(".choose").fadeOut(1);
        $(".back").fadeOut(1);
        cmpt="×";
        user="O";
        pcStep();
    });

    //电脑下棋,只下一步
    var pcStep = function(){
        var step = 0;       //记录当前是电脑下的第几步
        for(var i in group){
            if(group[i] !== 0){
                //如果某格已经下过了,step++
                step++;
            }
        }
        if(step %2 !== 0){
            //如果用户还没下,就return
            return;
        }

        if(step === 0){
            //如果电脑当前需要下第一步,因为是第一步所以不需要考虑该位置是否被别人下过的问题
            var proStep = [0,2,6,8,4];  //第一步允许下的地方,四个角与*,这里用的是从0开始
            var posit = parseInt(Math.random()*5,10);       //从0-4中随机生成一个数,作为proStep的下标,即随机选择一个格子下棋
            group[proStep[posit]] = 1;          //电脑下棋
            $("#span-"+(proStep[posit]+1)).html(cmpt);

            judge();
            return;
        }

        if(step === 2){
            //如果是电脑下的第二步,分两种情况,分别是电脑第一步下了正中和四个角
            if(group[4] === 1){
                //如果电脑第一步下的正中,又分两种情况,对方下的是四个角还是中间
                var corStep = [0,2,6,8];    //四个角在group的索引
                for(var t = 0; t<4;t++){
                    if(group[corStep[t]] === 2){
                        //如果玩家下的是某一个角,那就下他对角
                        var posit = 0;      //这里表示的是电脑要下的位置
                        if(corStep[t] === 0){
                            posit = 8;
                        }else if(corStep[t] === 8){
                            posit = 0;
                        }else if(corStep[t] === 2){
                            posit = 6;
                        }else if(corStep[t] === 6){
                            posit = 2;
                        }
                        posit = parseInt(posit);
                        group[posit] = 1;   //电脑下棋
                        $("#span-"+(posit+1)).html(cmpt);
                        judge();
                        return;
                    }
                }
                //电脑下的不是某个角,而是在每一行或列的中间位置,电脑就下一个靠着它的角
                var posit_g=[0,0];  //如果下在中间,就会有两个角靠着它,随机选一个
                var posit = 0;              //这就是随机选择之后的位置
                if(group[1] === 2){
                    posit_g[0] = 0;
                    posit_g[1] = 2;
                }else if(group[3] === 2){
                    posit_g[0] = 0;
                    posit_g[1] = 6;
                }else if(group[5] === 2){
                    posit_g[0] = 2;
                    posit_g[1] = 8;
                }else if(group[7] === 2){
                    posit_g[0] = 6;
                    posit_g[1] = 8;
                }
                posit = posit_g[parseInt(Math.random()*2)];
                posit = parseInt(posit);
                group[posit] = 1;   //电脑下棋
                $("#span-"+(posit+1)).html(cmpt);
                judge();
                return;
            }else{
                //如果电脑第一步下的不是正中,而是四个角
                //分两种情况,如果对方没下正中,那么就下正中,如果对方下了正中,就下第一步的对角
                if(group[4] === 0){
                    //玩家没下正中,则下正中
                    group[4] = 1;
                    $("#span-5").html(cmpt);
                    judge();
                    return;
                }
                //下第一步的对角
                var posit = 0;  //记录要下的从0起的位置
                if(group[0] === 1){
                    posit = 8;
                }else if(group[8] === 1){
                    posit = 0;
                }else if(group[2] === 1){
                    posit = 6;
                }else if(group[6] === 1){
                    posit = 2;
                }
                posit = parseInt(posit);
                group[posit] = 1;   //电脑下棋
                $("#span-"+(posit+1)).html(cmpt);
                judge();
                return;
            }
        }

        /*如果是第二步以后,分三步
        * 1、判断自己是否可以三连珠,如果可以就连上就赢了
        * 2、判断对方是否可以三连珠,如果有就堵上,不然就输了
        * 3、遍历剩下的还没下的格子,看下在哪一个,能让自己一条线上存在两粒棋子且都能实现三连珠的  这样的线最多      
        */

        //第一步
        var first_arr = checkThree(1,group);
        if(first_arr.length !== 0){
            //如果自己可以三连珠
            var posit = first_arr[0];
            posit = parseInt(posit);
            group[posit] = 1;   //电脑下棋
            $("#span-"+(posit+1)).html(cmpt);
            judge();
            return;
        }
        //如果自己不能三连珠,就第二步,检查对方是否能三连珠
        var second_arr = checkThree(2,group);
        if(second_arr.length !== 0){
            //如果对方可以三连珠
            //console.log(second_arr[0]);
            var posit = second_arr[0];
            posit = parseInt(posit);
            group[posit] = 1;   //电脑下棋
            $("#span-"+(posit+1)).html(cmpt);
            judge();
            return;
        }
        //如果自己和对方都不能三连珠,进入第三步
        var third_posit = 0;
        var third_max = -1;
        for(var temp in group){
            if(group[temp] === 0){
                if(third_max === -1){
                    third_posit = temp;
                    third_max = 0;
                }
                var ttt = [].concat(group);
                ttt[temp] = 1;
                var temp_arr = checkThree(1,ttt);
                if(temp_arr.length > third_max){
                    //如果当前点能让更多个连珠,就决定是它了
                    third_max = temp_arr.length;
                    third_posit = temp;
                }
            }
        }
        group[third_posit] = 1; //电脑下棋
        //这里必须先转成数字,不然会当成字符串相加,会不对
        var wtf = parseInt(third_posit);
        wtf+=1;
        $("#span-"+wtf).html(cmpt);
        judge();
        return;
    };

    //检查是否有一点可以三连珠,参数kind如果是1,检查电脑,参数如果是2,检查玩家,如果检查到下某一点可以三连珠,
    //就返回该格从0开始的下标(数组形式,所有情况都在),如果没找到,返回[]
    //参数gp是参考的数组,正常情况下就是group,但是考虑到第三步需要判断每一个格子的,那时候要传一个临时数组了
    var checkThree = function(kind,gp){
        var situ = [];          //用来记录所有需要返回的值
        var allPossible = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];        //记录八条线
        for(var i in allPossible){
            var x = allPossible[i][0];
            var y = allPossible[i][1];
            var z = allPossible[i][2];
            if((gp[x] === kind && gp[y] === kind && gp[z] === 0) || (gp[x] === 0 && gp[y] === kind && gp[z] === kind) || (gp[x] === kind && gp[y] === 0 && gp[z] === kind)){
                //迭代判断吧,就不复制粘贴了
                //如果满足上述条件
                //console.log("Three:"+allPossible[i]);
                if(gp[x] === 0){
                    situ.push(x);
                    continue;
                }else if(gp[y] === 0){
                    situ.push(y);
                    continue;
                }else if(gp[z] === 0){
                    situ.push(z);
                    continue;
                }
            }
        }
        return situ;
    };


    //输赢平的结果显示  state取值1,2,3,1表示电脑赢,2表示玩家赢,3表示平局,a,b,c表示连起来的三格
    var result = function(state,a,b,c){
        if(state === 1){
            console.log('lose');
            $(".loser").html("LOSE!");

        }else if(state === 2){
            console.log('win');
            $(".loser").html("WIN!");

        }else if(state === 3){
            console.log('tie');
            $(".loser").html("TIE!");

        }
        if(state !== 3){
            $("#tic-"+a).css("background-color","#877f6c");
            $("#tic-"+b).css("background-color","#877f6c");
            $("#tic-"+c).css("background-color","#877f6c");
        }
        setTimeout(function(){
            if(state !== 3){
                $("#tic-"+a).css("background-color","#fff");
                $("#tic-"+b).css("background-color","#fff");
                $("#tic-"+c).css("background-color","#fff");
            }
            $(".loser").fadeIn(400,function(){
                    setTimeout(function(){
                    beginAgain();
                },2000);
            });
        },1500);



    };

    //出了结果就重新开始
    var beginAgain= function(){
        for(var yyy = 0; yyy < 9;yyy++){
            group[yyy]=0;
            $("#span-"+(yyy+1)).html("");
        }
        $(".loser").fadeOut(1,function(){
            pcStep();
        });

    }

    //判断输赢与和棋,一共10种情况,8负1平1还没下完
    var judge = function(){
        if(group[0] === group[1] && group[1] === group[2] && group[0]!== 0){
            //第一行连起来了,这样写是因为格子的编号是从0开始,
            result(group[0],1,2,3);
        }else if(group[3] === group[4] && group[4] === group[5] && group[3] !== 0){
            //第二行
            result(group[3],4,5,6);
        }else if(group[6] === group[7] && group[7] === group[8] && group[6] !== 0){
            //第三行
            result(group[6],7,8,9);
        }else if(group[0] === group[3] && group[3] === group[6] && group[0] !== 0){
            //第一列
            result(group[0],1,4,7);
        }else if(group[1] === group[4] && group[4] === group[7] && group[1] !== 0){
            //第二列
            result(group[1],2,5,8);
        }else if(group[2] === group[5] && group[5] === group[8] && group[2] !== 0){
            //第三列
            result(group[2],3,6,9);
        }else if(group[0] === group[4] && group[4] === group[8] && group[0] !== 0){
            //主对角线
            result(group[0],1,5,9);
        }else if(group[2] === group[4] && group[4] === group[6] && group[2] !== 0){
            //次对角线
            result(group[2],3,5,7);
        }else{
            //没分出胜负
            var isTie = true;
            for(var i = 0; i < 9;i++){
                if(group[i] === 0){
                    //还有格子没下,表示棋还没下完
                    isTie = false;
                }
            }
            //平局
            if(isTie){
                result(3,0,0,0);
            }else{
                var step = 0;       //记录当前是电脑下的第几步
                for(var i in group){
                    if(group[i] !== 0){
                    //如果某格已经下过了,step++
                        step++;
                    }
                }
                if(step %2 === 0 && step !== 0){
                    //如果用户下了,就电脑下
                    pcStep();   
                }
            }
        }
    };


    //初始化棋盘点击事件,闭包以获取对应的o
    var initClick = function(i){
        $("#tic-"+i).on("click",function(){
            var step = 0;       //记录当前是下的第几步
            for(var j in group){
                if(group[j] !== 0){
                    //如果某格已经下过了,step++
                    step++;
                }
            }
            if(step %2 === 0){
                //如果电脑还没下,点了也没用
                return;
            }


            if(group[i-1] === 0){
                //如果没下,那么按了才有效,并将之设置为玩家下的,并判断是否和棋或者赢了
                //console.log("pressed:"+i);
                //console.log(group);
                group[i-1] = 2;
                $("#span-"+i).html(user);
                judge();
            }
        });
    };

    for(var i=1;i<=9;i++){
        initClick(i);       //初始化棋盘点击事件 
    }




});



欢迎大家加入QQ群一起交流讨论,「吟游」程序人生——YinyouPoet