简易五子棋&权值AI(1)
会写ui之后,自己写五子棋游戏可能并不是很难。尤其是在写完之后,你会发现其实也就几百行代码的事。下面简单回忆下我写五子棋的过程,当然,我所实现的就是最基础的双人对弈和利用权值法实现的AI五子棋。
整体步骤
我整体的目标就是实现简单界面双人对弈和人机对弈的功能。大家自己来写的话,其实最好的是写之前就先要明白最后想达到怎么样的效果,这样才有助于整体的设计,避免后续再反复整改。
1.首先绘制最简易的五子棋界面,画出棋盘,实现落子,悔棋等基本的功能。
2.实现黑白子轮流落子,并实现对棋局输赢的判断。
实现这一步后,其实就实现了双人对弈。
3.利用权值法实现AI五子棋(人机对弈)。
到这一步,我们的功能就以基本实现,接下来要做的,就是美化一下我们的界面以及“游戏体验”。
这是我的下棋界面:
我一开始的目标大致就是这样,不过我还想有背景音乐和计时器,不过没有去实现。
(下面所有的代码都是我写完整改好后的,所以代码很凌乱,我只将实现功能的部分尽量粘贴出来解说,完整源码和图片会放在文末,不过我的代码可能有点不大好看,勿喷,谢。)
一、界面,落子,悔棋
1.界面基础设置:
界面类继承JFrame
setSize(1000,700);//界面大小
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//界面关闭,程序运行结束
setTitle("五子连珠");//界面名
setLocationRelativeTo(null);//界面置屏幕中央
setLayout(null);//非必要
setIconImage(goBangIcon);//程序运行时的图标
setVisible(true);//界面可视化
graphic=getGraphics();//获得界面的画布
说明:
由于我最后所有东西都是直接用画布画出来的,所以界面布局方式不用设置也可。当然,如果用系统JButton等组建的话,设置布局还是必要的。
Position是一个简单的棋子位置类,保存棋子的行列
class Position
{
public Position(int Row,int Col)
{
row=Row;
col=Col;
}
public int row,col;
}
用一个二维数组来记录棋盘的整体情况,recond栈用来记录下棋。
Stack<Position > recond; //下棋记录
int [][]boolMap;//记录棋盘的落子情况,无子为0,黑子为1,白子为-1;
在画棋盘之前,要明确的是这个棋盘到底需要哪些参数来控制,就是说觉得后续过程中或者游戏中可以改变的,尽量都设置成参数,这样方便配置。
Graphics graphic;
int uiWidth,uiHeight;
int halfSquareSize; //棋盘方格大小
int pieceRadius; //棋子半径
int rows,cols; //线的行列(不是格)
private int checkBoardWidth,checkBoardHeight;//棋盘的宽高
int playTurn; //下棋一方
int comFirst; //电脑是否先手,先手为1;不先手为-1;双人时为0;
int couldGame;//是否可以游戏
我觉得双人对弈和人机对弈本身没有太大区别,所以将他们放在一起写,用comfirst来区别。而playTurn参数是执子方,初始值为1(黑子),即无论人机还是双人,一定先是黑子落子。
2.画出棋盘:
//棋盘位置
int []cBPos=new int[4];
public void paintGameUi()
{
//绘制游戏界面
graphic.drawImage(backGround,0,0,uiWidth,uiHeight,null);//界面背景
graphic.drawImage(checkBoardBG,cBPos[0],cBPos[1],checkBoardWidth,checkBoardHeight,null);//棋盘背景
//画边
graphic.drawRect(cBPos[0], cBPos[1],checkBoardWidth,checkBoardHeight);
//画格线
for(int haveDrawRow=0;haveDrawRow<rows;haveDrawRow+=1)
{
graphic.drawLine(cBPos[0]+halfSquareSize,cBPos[1]+(2*haveDrawRow+1)*halfSquareSize,cBPos[0]+checkBoardWidth-halfSquareSize,cBPos[1]+(2*haveDrawRow+1)*halfSquareSize);
}
for(int haveDrawCol=0;haveDrawCol<cols;haveDrawCol+=1)
{
graphic.drawLine(cBPos[0]+(2*haveDrawCol+1)*halfSquareSize,cBPos[1]+halfSquareSize,cBPos[0]+(2*haveDrawCol+1)*halfSquareSize,cBPos[1]+checkBoardHeight-halfSquareSize);
}
//画出所有已有棋子
int tempTurn=1;
if(!recond.empty())
{
for(int i=0;i<recond.size();i+=1)
{
drawOnePiece(recond.get(i).row,recond.get(i).col,tempTurn);
tempTurn=(tempTurn==1?-1:1);
}
}
}
public void drawOnePiece(int Row,int Col,int tempTurn)
{
int pieceStartX=cBPos[0]+(1+Col*2)*halfSquareSize-pieceRadius,
pieceStartY=cBPos[1]+(1+Row*2)*halfSquareSize-pieceRadius;
if(tempTurn==1)graphic.drawImage(blackPiece,pieceStartX,pieceStartY,2*pieceRadius,2*pieceRadius,null);
else graphic.drawImage(whitePiece,pieceStartX,pieceStartY,2*pieceRadius,2*pieceRadius,null);
}
//注:棋子本身也是一个图片。
2.落子
给界面添加鼠标事件监听器,获得每次鼠标所点击的在界面上的位置。。。。这里最主要的问题是,如何判断鼠标点击的位置需不需要落子。
鼠标点击的位置需不需要落子?
首先,需要判断点击的位置是不是棋盘上,不是的话直接返回。
如果游戏已经结束,函数返回。
点击的是棋盘区域,那么:
这里我的设置是如果点击的是以棋盘行列线交汇点为中心的一个固定大小的圆(半径为recognizable)内,那么认为点击的就是该棋子位置。而获得这个点击的棋子位置并不难,是简单的数学问题。
(clickx和clicky是鼠标点击的界面位置)
float recognizable=(float) (pieceRadius*0.8);
int xRemander,yRemander;
xRemander=(int)Math.abs(halfSquareSize-(clickX-cBPos[0])%(2*halfSquareSize));
yRemander=(int)Math.abs(halfSquareSize-(clickY-cBPos[1])%(2*halfSquareSize));
if(xRemander*xRemander+yRemander*yRemander>recognizable*recognizable)return;
int clickRow,clickCol;
clickRow=(int)((clickY-cBPos[1])/(2*halfSquareSize));
clickCol=(int)((clickX-cBPos[0])/(2*halfSquareSize));
if(boolMap[clickRow][clickCol]!=0)return;//该位置已有棋子
//添加记录
recond.add(new Position(clickRow,clickCol));
boolMap[clickRow][clickCol]=playTurn;
drawOnePiece(clickRow,clickCol,playTurn); //画出棋子
其实,每次落子之后,都需要对棋局进行一次游戏是否结束的判断,判断函数后面再提。
3.悔棋
//悔棋
public void returnLast()
{
if(recond.empty())return;
Position pos=recond.pop();//弹出栈顶元素,即最近下的一枚棋子
paintGameUi();//重绘界面
boolMap[pos.row][pos.col]=0;//该位置置空
playTurn=(playTurn==1?-1:1);//改变执手方
computerFirst(); //函数里会判断是否需要电脑先手
}
computerFirst()函数是人机对弈时悔棋到最初始时,且电脑先手,那么需下出第一颗棋子。我设置的电脑先手落子的位置为棋盘中央区域内的任意位置。
//电脑先手
public void computerFirst()
{
if(comFirst!=1||recond.size()!=0)return ;
Random ran=new Random();
int Row=ran.nextInt(6)+3;
int Col=ran.nextInt(6)+3;
boolMap[Row][Col]=playTurn;
drawOnePiece(Row,Col,playTurn);
//添加记录
recond.add(new Position(Row,Col));
playTurn=-1;//电脑先手为黑手,落子完后要改变执手方
}
二、判断输赢
循环判断以每个位置为中心的“米”字线上的四条线有无五子相连的(有则输赢已定),以及需判断棋盘是否已满(平局)。(注意:最后一颗棋子落子后,有可能有赢的一方,所以输赢判断优先于平局判断)。
public int isOver(int Row,int Col)
{
//不结束返回为0,黑胜返回1,白胜返回-1,棋盘满平局返回2
int win=1;
//纵向
for(int i=Row-1;i>=0;i-=1)
{
if(boolMap[i][Col]==boolMap[Row][Col])win+=1;
else break;
}
for(int i=Row+1;i<rows;i+=1)
{
if(boolMap[i][Col]==boolMap[Row][Col])win+=1;
else break;
}
if(win>=5)return playTurn;
else win=1;
//横向
for(int i=Col-1;i>=0;i-=1)
{
if(boolMap[Row][i]==boolMap[Row][Col])win+=1;
else break;
}
for(int i=Col+1;i<cols;i+=1)
{
if(boolMap[Row][i]==boolMap[Row][Col])win+=1;
else break;
}
if(win>=5)return playTurn;
else win=1;
//左斜
for(int i=Row-1,j=Col-1;i>=0&&j>=0;i-=1,j-=1)
{
if(boolMap[i][j]==boolMap[Row][Col])win+=1;
else break;
}
for(int i=Row+1,j=Col+1;i<rows&&j<cols;i+=1,j+=1)
{
if(boolMap[i][j]==boolMap[Row][Col])win+=1;
else break;
}
if(win>=5)return playTurn;
else win=1;
//右斜
for(int i=Row-1,j=Col+1;i>=0&&j<cols;i-=1,j+=1)
{
if(boolMap[i][j]==boolMap[Row][Col])win+=1;
else break;
}
for(int i=Row+1,j=Col-1;i<rows&&j>=0;i+=1,j-=1)
{
if(boolMap[i][j]==boolMap[Row][Col])win+=1;
else break;
}
if(win>=5)return playTurn;
//平局判断
if(recond.size()==rows*cols)return 2; //棋盘已满,和局
else return 0;
}
到这里,双人对弈就基本实现了。权值AI的部分写在下一篇。