小案例 JavaScript-简易五子棋

        前些日子跟着大佬试着写了个人机对战的五子棋游戏。闲暇之时对其做了一点点改进,于此进行整理;

        主要过程及思路 包括以下几个方面:

         1. 绘制棋盘;
         2. 确定如何在棋盘上落子;
         3. 确定 触发 游戏结束(gameover)的条件;
         4. 用户 落子触发的事件;
         5. 机器(AI) 落子触发的事件;

1. 绘制棋盘:

        在网页上显示的话,canvas肯定是最合适的选项:

h5部分:

<body>
	<h3 id="title">--五子棋--</h3>
	<canvas class="chessB" width="450px" height="450px"></canvas>
    <div>
    	<a id="restart">重新开始</a>
    </div>
</body>

CSS部分:

<style>
	body{
		background-color:#fff;
	}
	h3{
		text-align:center;
		font-family:"楷体";
	}
	.chessB{
		cursor:pointer;
		display:block;
		margin:50px auto;
		box-shadow:5px 5px 5px #B9B9B9,-2px -2px 2px #efefef;
	}
	div{
		text-align:center;
	}
	div>a{
		cursor:pointer;
		font-family:"宋体";
		color:#fff;
		padding:10px 20px;
		background-color:#F3F;
		border-radius:7%;
	}
</style>

JS部分:

var chess = document.getElementsByClassName("chessB")[0];
var context = chess.getContext("2d");
context.strokeStyle="#7A7A7A";

for(var i=0;i<15;i++){
	//设置横线
	//设置起始点的坐标
	context.moveTo(15,15+i*30);
	//设置结束点的坐标
	context.lineTo(435,15+i*30);
	//将两点连接起来
	context.stroke();
				
	//设置竖线
	//设置起始点的坐标
	context.moveTo(15+i*30,15);
	//设置结束点的坐标
	context.lineTo(15+i*30,435);
	//将两点连接起来
	context.stroke();
}

在这里插入图片描述

2. 确定如何在棋盘上落子:

         需要封装一个带三个参数的函数;
         三个参数分别为 落子坐标的x,y值,以及判断此时是 用户落子(黑子) 还是 AI落子(红子);

//落子方法
function oneStep(i,j,me){
	context.beginPath();
	context.arc(15+i*30,15+j*30,13,0,2*Math.PI);
	context.closePath();
	
	var color;
	if(me){
		color="#000";
	}
	else{
		color="red";	
	}
	context.fillStyle=color;
	context.fill();
}//oneStep

3.确定 触发 游戏结束(gameover)的条件

         智商正常的人都晓得五子棋四个方向(横,竖,上斜,下斜)上五个相同颜色棋子连成一片即为获胜;
        所以这里需要创建一个二维数组来统计棋盘上每个点可能的每种获胜办法。为了后边更方便的遍历所有的获胜方法,给数组再加上一个维度——用以存储该获胜方法的编号;

var wins=[];
for(var i=0;i<15;i++){
	wins[i]=[];
	for(var j=0;j<15;j++){
		wins[i][j]=[];
	}
}
			
var count=0;//赢法的编号
//统计横线赢法
for(var i=0;i<15;i++){
	for(var j=0;j<11;j++){
		for(var k=0;k<5;k++){
			wins[j+k][i][count]=true;
		}
		count++;//找到一个赢法后,count++;
	}
}
//统计竖线赢法
for(var i=0;i<15;i++){
	for(var j=0;j<11;j++){
		for(var k=0;k<5;k++){
			wins[i][j+k][count]=true;
		}
		count++;
	}
}
//统计正斜线赢法(向下斜)
for(var i=0;i<11;i++){
	for(var j=0;j<11;j++){
		for(var k=0;k<5;k++){
			wins[i+k][j+k][count]=true;
		}
		count++;	
	}	
}
//统计反斜线赢法(向上斜)
for(var i=0;i<11;i++){
	for(var j=14;j>3;j--){
		for(var k=0;k<5;k++){
			wins[i+k][j-k][count]=true;
		}
		count++;	
	}
}

         这样,后边去写用户落子检测是否达到胜利条件的时候,只需判断当前落下的棋子 所在的 行 列 斜线 上,是否有五个win值为true的节点连在一起即可;
         酱讲明起来可能会有点抽象,迷糊的话可以看 之后的图示。


4. 用户 落子触发的事件:

         用户落子时要满足以下条件:(1)落子的坐标上没有别的棋子;(2)此刻是用户落子的回合;(3)游戏尚未结束;

//定义二维数组--标记棋盘上的每个坐标上是否已经下了棋子
var chessBoard=[];
for(var i=0;i<15;i++){
	chessBoard[i]=[];
	for(var j=0;j<15;j++){
		chessBoard[i][j]=0;		
	}
}

var me=true;//判断人是否可以下棋
var over=false;//判断游戏是否结束

         同时,为了更加具象地表明 当前的落子 在 所处的 所有获胜方法中,已经有多少个先前落下的棋子满足获胜条件了(将其视作“分值”),需创建一个数组用以存储分值;

var myWin=[];//记录用户在某获胜方法上的分值
var aiWin=[];//记录计算机在某获胜方法上的分值
for(var t=0;t<count;t++){
	myWin[t]=0;	
	aiWin[t]=0;
}

         接下来就是用户落子的具体实现:

chess.onclick=function(e){
	//如果游戏已经结束就不能继续下棋
	if(over){
		return;
	}
	//是否轮到人下棋
	if(!me){
		return;	
	}
	//获取x轴坐标
	var x=e.offsetX;
	//获取y轴坐标
	var y=e.offsetY;
				
	var i=Math.floor(x/30);
	var j=Math.floor(y/30);
				
	if(chessBoard[i][j]==0){
		//下一个子
		oneStep(i,j,me);
		//已经落子
		chessBoard[i][j]=1;	
					
		for(var k=0;k<count;k++){
			if(wins[i][j][k]){
				myWin[k]++;
				if(myWin[k]==5){
					title.innerHTML="--恭喜您获胜了!--";
					over=true;
				}
			}	
		}
	}
							
	//计算机落子方法
	//玩家还没有赢的时候
	if(!over){
		me=!me;//玩家此时不能下棋
		AI();
	} 
}//chess.onclick

         举个 myWin[ ] 值变化的例子:
在这里插入图片描述
         这里原先有一个棋子,设它坐标为(x,y),我们假设此时要让它向右连成5个(此种获胜方法编号设为k)
         则,myWin[k] 从原来的0 ++,变成1(代表该种赢法上已经有一个棋子在了);
在这里插入图片描述
         我们在原来棋子的右侧又下了一个子,则该子的 win[x][y][k] 值会变为true (x,y为原来棋子的坐标,k为原来棋子向右连续5个的赢法);
        而一旦检测到 k 赢法上多了一个值为true的棋子后,myWin[k] 值再次加一,从原来的1 变成 2(代表该种赢法上有两个个棋子了);
        以此推论,当我在原来棋子的向右 4格之内的位置继续摆棋,myWin[k] 的值也始终在增加,当摆到5个棋子时,即myWin[]的值达到5时,即宣告游戏结束,用户方获得了最后胜利;
        事实上,当某处落下一枚棋子时,它影响到的绝对不止一种算法,在这里插入图片描述
        例如此图,当该子落下时,在所有的赢法里,有三种赢法对应的myWin 值等于2,而又有n种赢法的值等于1(n可以数出来的,我懒~);
        当然,在用户落子的函数最后,不要忘记添加一个游戏尚未结束的判断,以便让计算机收到可以开始落子的讯息;

//玩家还没有赢的时候
	if(!over){
		me=!me;//玩家此时不能下棋
		AI();//此为计算机落子的函数
	} 

5. 机器(AI) 落子触发的事件:

        (PS:AI落子涉及到一个权重问题,我看到很多大佬写的AI落子风格更加激进,以进攻为首要目标,其次才是防守拦截;当我按着这个思路去完成代码并测试时,发现AI经常莫名其妙地变成了一个比IG还IG的莽夫,有时候甚至无视用户已经落下的三子(会玩五子棋的都懂这是啥意思),而选择强行进攻~为了避免这点,我更倾向于AI以防守为主,虽然有时候会铁憨憨似的放弃绝妙的进攻机会,但延长游戏时间也算给用户带来了更好的体验;)
        为了让AI落子更有逻辑性,我们需要创建两个二维数组,分别代表用户棋子所在位置的“威胁度”,以及AI棋子所在位置的“威胁度”(“威胁度”不难理解,某个赢法上myWin值到达4时,即为较高威胁度)

var myScore=[];//存储 未落子处 用户的赢法的威胁度
var aiScore=[];//存储 未落子处计算机的赢法的威胁度
				
//初始化威胁度
for(var i=0;i<15;i++){
	myScore[i]=[];
	aiScore[i]=[];				
	for(var j=0;j<15;j++){
		myScore[i][j]=0;
		aiScore[i][j]=0;	
	}
}

        每一次AI落子时,都要遍历整个棋盘,对所有未落子的点进行威胁度检测,即 己方四子就绪 威胁度最高,其次为 对方四子就绪…以此推类:
        这里还涉及到一个问题,例如,当用户有两处地方所落下的棋子威胁度相同时:AI要在其中选择出一处更利于己方进攻的;同理,当进攻选择出现相同威胁度时,要挑选出一处更利于防守的;

for(var i=0;i<15;i++){
	for(var j=0;j<15;j++){
		//判断是否是空白子
		if(chessBoard[i][j]==0){
			for(var k=0;k<count;k++){
				if(wins[i][j][k]){					
					//拦截
					if(myWin[k]==1){
						myScore[i][j]+=200;
					}
					else if(myWin[k]==2){
						myScore[i][j]+=400;
					}	
					else if(myWin[k]==3){
						myScore[i][j]+=2000;
					}	
					else if(myWin[k]==4){
						myScore[i][j]+=10000;
					}
									
					//进攻
					if(aiWin[k]==1){
						aiScore[i][j]+=190;	
					}
					else if(aiWin[k]==2){
						aiScore[i][j]+=390;
					}	
					else if(aiWin[k]==3){
						aiScore[i][j]+=1800;
					}
					else if(aiWin[k]==4){
						aiScore[i][j]+=20000;
					}
				}									
			}
							
			if(myScore[i][j]>max){
				max=myScore[i][j];
				x=i;
				y=j;	
			}
			else if(myScore[i][j]=max){
				//面对用户下的子所带来的威胁是同一级别时,计算机优先去在对自己更有利的地方进行拦截(顺便进攻)
				if(aiScore[i][j]>max){
					max=aiScore[i][j];
					x=i;
					y=j;
				}	
			}
							
			if(aiScore[i][j]>max){
				max=aiScore[i][j];
				x=i;
				y=j;	
			}
			else if(aiScore[i][j]=max){
				if(myScore[i][j]>max){
					//道理同上,进攻的机会出现同一级别时,选择更有利防守的下棋方法
					max=myScore[i][j];
					x=i;
					y=j;
				}	
			}							
		}	
	}	
}

        当然,最后不要忘记让AI完成页面上的落子,并判断是否满足胜利条件,以及修改me值让用户继续落子;
        整个AI落子函数如下所示:

function AI(){
	var myScore=[];
	var aiScore=[];
	
	for(var i=0;i<15;i++){
		myScore[i]=[];
		aiScore[i]=[];
		
		for(var j=0;j<15;j++){
			myScore[i][j]=0;
			aiScore[i][j]=0;	
		}
	}
	
	var max=0;
	var x=0,y=0;
	
	for(var i=0;i<15;i++){
		for(var j=0;j<15;j++){
			if(chessBoard[i][j]==0){
				for(var k=0;k<count;k++){
					if(wins[i][j][k]){
						
						if(myWin[k]==1){
							myScore[i][j]+=200;
						}
						else if(myWin[k]==2){
							myScore[i][j]+=400;
						}	
						else if(myWin[k]==3){
							myScore[i][j]+=2000;
						}	
						else if(myWin[k]==4){
							myScore[i][j]+=10000;
						}
						
						if(aiWin[k]==1){
							aiScore[i][j]+=190;	
						}
						else if(aiWin[k]==2){
							aiScore[i][j]+=390;
						}	
						else if(aiWin[k]==3){
							aiScore[i][j]+=1800;
						}
						else if(aiWin[k]==4){
							aiScore[i][j]+=20000;
						}
					}									
				}
				
				if(myScore[i][j]>max){
					max=myScore[i][j];
					x=i;
					y=j;	
				}
				else if(myScore[i][j]=max){
					if(aiScore[i][j]>max){
						max=aiScore[i][j];
						x=i;
						y=j;
					}	
				}
				
				if(aiScore[i][j]>max){
					max=aiScore[i][j];
					x=i;
					y=j;	
				}
				else if(aiScore[i][j]=max){
					if(myScore[i][j]>max){
						max=myScore[i][j];
						x=i;
						y=j;
					}	
				}
			}	
		}	
	}
	
	
	//计算机落子
	oneStep(x,y,me);
	chessBoard[x][y]=1;
	
	//判断计算机有没有赢
	for(var k=0;k<count;k++){
		if(wins[x][y][k]){
			aiWin[k]++;
			if(aiWin[k]==5){
				title.innerHTML="--很遗憾,是电脑获胜了!--";
				over=true;
			}
		}	
	}
	
	//计算机下完子,把me改为true,意思是玩家可以继续下子
	if(!over){
		me=!me;
	}
}

        源码刚刚上传,还在审核,等审核完毕了就能从笔者主页找到啦。
        顺便吐槽一句,改了AI算法以后的计算机属实狠人,赢似乎是不想赢的,就怼着我的棋一通拦截,两个也拦,没的商量的那种,最后莫名其妙的它就一不小心到5个了?excuse me…
        附上被虐截图:
在这里插入图片描述
——————————————————————————— END ————————————————————————————

猜你喜欢

转载自blog.csdn.net/weixin_44990584/article/details/106297375
今日推荐