网页版井字游戏(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、遍历所有还没下棋的格子,对每一个格子进行判断,并统计出如果下这一格,能让几条线有二连珠且第三个位置为空位。选择统计的数量最多的一格下棋。有点抽象,拿下图举个例子,图中编号为下棋的顺序:
这里写图片描述
看第五步棋,也就是电脑的第三步,如果该棋下在第一行中间位置,能成线的条数为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

posted @ 2017-08-13 23:07  _吟游诗人  阅读(880)  评论(0编辑  收藏  举报