网页版井字游戏(TicTacToe)人机对战的制作(附思路和源码)
井字游戏的规则是:在一个井字格子的棋盘里下棋,横竖斜一旦三子连子,则胜。而事实上,遵循一定的规则,该游戏便能保证不败,即至少是平局。
若是两人对战,则仅需要判断“胜负平”三种状态即可,比较简单,而人机对战的难点便在于让机器立于不败之地的下棋规则。下面会重点讲解不败的思路。
先放一张游戏截图,程序演示与源码下载可以去:戳我演示或下载代码
在此先规定电脑一定是先手,如果电脑不是先手的话算法需要另外设计,但方法类似,在此暂不讨论。先说说电脑先手时保持不败的设计思路:
根据下图标出的编号来看:
前两子是有特别讲究的,不然可能会输。
第一子:只能在正中间或者四个角随机选一个,即1、3、5、7、9中任选一个。
第二子:分两种情况讨论。
若第一子在正中,即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(随机选择)若第一子在四个角,即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); //初始化棋盘点击事件
}
});