五子棋(人机对战)
下过五子棋的人都知道它的整个流程。首先,要有棋盘,棋子(分两种颜色),然后才是具体下棋的步骤。
1.首先要得到棋盘。棋盘是由多条横线和竖线组成的。我们得到窗体上的画布对象g就可以画出我们需要的线条了。
//画棋盘 public void chessP(Graphics g){ //画棋盘的方法 //画10条横线 for(int i=0;i<12;i++){ g.drawLine(50,50+50*i,50+(12-1)*50,50+50*i); } //画10条竖线 for(int j=0;j<12;j++){ g.drawLine(50+50*j,50,50+50*j,50+(50-1)*50); }
这样画好了线条就会发现,棋盘就是固定好的了,如果你想改变棋盘的大小就很不方便。特别是当你在其它很多地方用到了这些线条的数据。这就非常不好改动了。所以,我们可以用变量来表示这些常用到的数据。这样改动起来就显得方便了。
package FiveChess /** * 设置需要的参数 * @author Administrator * */ public class chessb { public static int x0=50; //棋盘的横坐标的起始点 public static int y0=50; //棋盘的纵坐标的起始点 public static int CELL_X=13; //棋盘的横线数 public static int CELL_Y=13; //棋盘的纵线数目 public static int CELL_SIZE=50; //格子的大小 }
我们把它们单独放在一个类里,将所需要的参数定义为static 变量,这样就用起来方便了。所以在画棋盘的时候,我们就把它改成以下这样,如果,还有其它地方也用到了这些数据,我们只要改变这个方法里的变量的值就好了。
//画棋盘
public void chessP(Graphics g){//画棋盘的方法
//画10条横线
for(int i=0;i<chessb.CELL_X;i++){ g.drawLine(chessb.x0,chessb.y0+chessb.CELL_SIZE*i,chessb.x0+(chessb.CELL_Y-1)*chessb.CELL_SIZE,chessb.y0+chessb.CELL_SIZE*i);
}
//画10条竖线
for(int j=0;j<chessb.CELL_Y;j++){ g.drawLine(chessb.x0+chessb.CELL_SIZE*j,chessb.y0,chessb.x0+chessb.CELL_SIZE*j,chessb.x0+(chessb.CELL_X-1)*chessb.CELL_SIZE);
}
2.棋盘画好了,我们才能在上面下棋子。所谓的画棋子就是我们需要在窗体上通过鼠标的点击来画圆。也就是说,要有鼠标监视器才能完成。
当我们用鼠标点击棋盘上的某一点时,在适当的位置画圆。所以就要得到鼠标点击的坐标位置。当然,不是随便点在棋盘上任何地方都可以画圆。这样就乱套了。我们希望出现的圆出现的位置是一致的,或者是在棋盘上的交叉点上,或者是在小方格里。这里我是暂时把它放在交叉点上。圆的圆心是要在各交叉点上,但不是我们每次都能恰好点击在交叉点上。所以,我们就需要一个范围,比如,在交叉点的1/3半径的圆圈内的点击都视为是有效的。范围适当就好,不要太大,太大就不能做出正确的判断了;当然也不能太小,太小也就没有意思了。
//放棋子 的方法 public void mouseReleased(MouseEvent e) { int x = e.getX(); // 获取鼠标的释放时的坐标 int y = e.getY(); for (int i = 0; i < chessb.CELL_Y; i++) for (int j = 0; j < chessb.CELL_X; j++) { tx = chessb.x0 + chessb.CELL_SIZE * j;// 获取交叉点的坐标 ty = chessb.x0 + chessb.CELL_SIZE * i; if (Math.abs(tx - x) <= chessb.CELL_SIZE / 3 && Math.abs(ty - y) <= chessb.CELL_SIZE / 3) {// 看鼠标点击的是否在以交叉点为圆心的1/3半径的圆的有效范围内 if (chess[i][j] == 0) { // 二维数组int默认的是0 g.setColor(Color.BLACK); chess[i][j]=1;//使黑色棋子的引用值为1 } else {g.setColor(Color.RED); chess[i][j]=-1;//使红色棋子的引用值为-1 } g.fillOval(tx-chessb.CELL_SIZE/2,ty-chessb.CELL_SIZE/2,chessb.CELL_SIZE,chessb.CELL_SIZE);//画圆 Black=!Black;//改变颜色 } } }
这是简单的放两种颜色的棋子的方法。也就是说这已经可以实现画两种颜色的棋子了。但是画多了,就会发现,当你的鼠标点击的地方已经有棋子的时候,它还是会在这个地方重新画一个圆。于是,我们就想到,是不是让已经有圆的坐标标记出来呢。可能标记的方法有很多,我会用的只有数组了。所以,我暂时用数组来标记了。所以可以定义一个和棋盘一样的行数和列数的二维数组来标记每个交叉点。(黑色棋子的值为1,红色的值为-1,其它的就为0)。所以,只要每次点击的交叉点上的值是0就可以解决这个问题了。
以上步骤做完了,那么我们就可以实现基本的下棋了。接下来就是,判断输赢的问题了。总不能一直下下去吧。
3. 我们也可以将判断的方法放在一个类里面。这样也便于自己整理。
下过五子棋的人都知道,只要有一方的棋子,在横,竖直,以及y=x,y=-x 等四个不同的任一方向有五子连在一起就赢了。所以,我们判断输赢的时候,需要从四个不同的方来析。
package FiveChess /** * 判断输赢的类 * @author Administrator * */ public class pand { private int[][] chess; public pand(int[][] chess){ //把主类的棋盘传过来 this.chess=chess; } public int count(int r,int c){//横向找棋子的方法 int count=0;//定义一个计数器 //向右边找与其相同的棋子 for(int i=c;i<chessb.CELL_Y;i++){ if(chess[r][c]==chess[r][i]){ count++;}//如果是相同的颜色则个数加1 else break;//如果颜色不同,则跳出此次循环 } //向左找与其相同的棋子 for(int i=c-1;i>=0;i--){ if(chess[r][c]==chess[r][i]){ count++; } else break; } return count; } public int count1(int r,int c){//纵向找相同棋子的方法 int count=0; for(int i=r;i<chessb.CELL_X;i++){//向下找连续相同的棋子 if(chess[i][c]==chess[r][c]){ count++;} else break; } for(int i=r-1;i>=0;i--){ if(chess[i][c]==chess[r][c]){//向上找 count++; } else break; } return count; // 返回纵向的连续的棋子 } public int count2(int r,int c){//找y=-x 方向(即从左上角到右下角)的棋子的方法 int count=0; for(int i=r,j=c;i<chessb.CELL_Y&&j<chessb.CELL_X;i++,j++){//往右下角方向找 if(chess[i][j]==chess[r][c]){ count++; } else break; } for(int i=r-1,j=c-1;i>=0&&j>=0;i--,j--){//往左上角方向找相同的棋子 if(chess[i][j]==chess[r][c]){ count++; } else break; } return count; } public int count3(int r,int c){//找从左下角到右上角方向的棋子方法 int count=0; for(int i=r,j=c;i>=0&&j<chessb.CELL_Y;i--,j++){//往右上角方向找连续相同的棋子 if(chess[i][j]==chess[r][c]){ count++; } else break; } for(int i=r+1,j=c-1;j>=0&&i<chessb.CELL_X;j--,i++){//往左下角方向找连续相同 的棋子 if(chess[i][j]==chess[r][c]){ count++; } else break; } return count; }}
可以做出输赢判断后,我们就可以在监听器中得出结果了。
pand pd=new pand(Five.chess);//创建一个判输赢的对象,并且给它传值
int n=pd.count(i,j);int n1=pd.count1(i, j);int n2=pd.count2(i, j);int n3=pd.count3(i, j);//以最后下的棋子为中心,分别从四个方向找与它颜色相同且连续的棋子的个数
if(n>=5||n1>=5||n2>=5||n3>=5){//判断是否有哪一个方向的个数达到5个了
if(Five.chess[i][j]==1){//看最高的棋子的颜色是否是黑色
JOptionPane.showMessageDialog(null, "黑色赢了!!");//弹出对话框
System.out.println(" 黑色赢了!!!");
}
else {
JOptionPane.showMessageDialog(null, "红色赢了!!");//弹出对话框
System.out.println(" 红色赢了!!!");}
}
}
4.现在是可以下棋子,也可以判断输赢了。但又会发现还有一个很大的问题就是,当把窗口隐藏了再打开时,之前的那些棋子又没了,对这就是因为我们没有为棋子重绘造成的。所以,我们还需要继续。以下就是棋子的重绘。在这里,我们可以很好的利用之前的二维数组。因为这是一个地址,所以,这里我们又可以拿来用。
public void paint(Graphics g){ super.paint(g);//继承父类的方法 chessP(g);//画棋盘 //保存,重绘 for(int i=0;i<chess.length;i++){ for(int j=0;j<chess[0].length;j++){ if(chess[i][j]!=0){//判断该交叉点是不是有棋子 int x=chessb.x0+chessb.CELL_SIZE*j;//获取交叉点的坐标 int y=chessb.y0+chessb.CELL_SIZE*i; if(chess[i][j]==1){//判断是否是黑色 g.setColor(Color.BLACK); } else { //判断是否是红色的 g.setColor(Color.RED); } g.fillOval(x- chessb.CELL_SIZE / 2, y- chessb.CELL_SIZE / 2,chessb.CELL_SIZE,chessb.CELL_SIZE);//画实心圆 } } } }
5.接下来是怎么实现人机的简单对战。简单的理解就是,自己用鼠标点击一次时,会出现两颗不同颜色的棋子。一颗是自己下的,另外一颗是机器根据自己下的位置来决定下的。这里最重要的是机器下棋子的问题了。也就是,怎么让机器根据自己下完的棋子后,整体考虑下在哪里。至于有多少方法实现,我也不清楚。老师说可以用权值来做。
其实在下棋的时候,也知道一般下棋是要看整个棋盘的棋子的。当然首先是自己的,在是对手的。具体点就是,1.看自己有么有四个连续的,再是对手有没有。2.先看自己有么有三个的,再考虑对方的三个的问题。这样推导下去。我个人觉得这个算法很有难度,我也只能做简单的几乎没什么智商的人机对战。
机器人在下的位置,必须是空坐标点,然后再判断该点是不是合理些。下面是判断任一空坐标点四周的棋子的情况。
package FiveChess;
/**
* 用来判断空坐标的四个不同方向的棋子数
* @author Administrator
*
*/
public class pandk{
private int[][] chess2;
public pandk(int[][] chess2){
this.chess2=chess2; //把保存权值的数组传过来
}
public int[] count(int r,int c){//横向找棋子的方法
int c1=0,c2=0; //定义两个计数器分别计算自己的和对方的
int[] cc=new int[2]; //定义一个二维数组,用来保存c1和c2的值
//向右边找相同红色的棋子
for(int i=c+1;i<chessb.CELL_Y;i++){
if(chess2[r][i]==-1){
c1++;} //如果是相同的红颜色则个数加1
else break; } //如果颜色不同,则跳出此次循环
//向左找相同红色的棋子
for(int i=c-1;i>=0;i--){
if(chess2[r][i]==-1){
c1++; } //如果是相同的红颜色则个数加1
else break; } //如果颜色不同,则跳出此次循环
for(int i=c+1;i<chessb.CELL_Y;i++){
if(chess2[r][i]==1){
c2++;} //如果是相同的黑颜色则个数加1
else break; } //如果颜色不同,则跳出此次循环
//向左找相同黑色的棋子
for(int i=c-1;i>=0;i--){
if(chess2[r][i]==1){
c2++; }
else break; }
//System.out.println("横向的机器人的有-->"+c1);
//System.out.println("横向的自己的有-->"+c2);
cc[0]=c1;cc[1]=c2;
return cc; }
public int[] count1(int r,int c){//纵向找相同棋子的方法
int c1=0,c2=0; //定义两个计数器分别计算自己的和对方的
int[] cc=new int[2]; //定义一个二维数组,用来保存c1和c2的值
for(int i=r+1;i<chessb.CELL_X;i++){//向下找连续红色的棋子个数
if(chess2[i][c]==-1){
c1++;}
else break; }
for(int i=r-1;i>=0;i--){
if(chess2[i][c]==-1){//向上找红色连续的棋子
c1++; } //如果是相同的红颜色则个数加1
else break; }//如果颜色不同,则跳出此次循环
for(int i=r+1;i<chessb.CELL_X;i++){//竖直向下找黑色的棋子
if(chess2[i][c]==1){
c2++;}
else break; }
for(int i=r-1;i>=0;i--){
if(chess2[i][c]==1){ //竖直向上找黑色的棋子
c2++; }
else break; }
// System.out.println("纵向的机器人的有---"+c1);
// System.out.println("纵向的自己的有---"+c2);
cc[0]=c1;cc[1]=c2;
return cc; // 返回纵向的连续的红色和黑色的棋子个数 }
public int[] count2(int r,int c){//找y=-x 方向(即从左上角到右下角)的连续棋子的方法
int c1=0,c2=0;
int[] cc=new int[2];
for(int i=r+1,j=c+1;i<chessb.CELL_Y&&j<chessb.CELL_X;i++,j++){//往右下角方向找红色棋子的个数
if(chess2[i][j]==-1){
c1++; }
else break; }
for(int i=r-1,j=c-1;i>=0&&j>=0;i--,j--){//往左上角方向找连续的红色的棋子个数
if(chess2[i][j]==-1){
c1++; }
else break; }
for(int i=r+1,j=c+1;i<chessb.CELL_Y&&j<chessb.CELL_X;i++,j++){//往右下角方向找连续 的黑色的棋子的个数
if(chess2[i][j]==1){
c2++; }
else break; }
for(int i=r-1,j=c-1;i>=0&&j>=0;i--,j--){//往左上角方向找连续的黑色的棋子个数
if(chess2[i][j]==1){
c2++; }
else break; }
//System.out.println("y=-x斜向的有:---"+c1);
cc[0]=c1;cc[1]=c2;
return cc; // 返回y=x方向的连续的红色的和黑色的棋子个数 }
public int[] count3(int r,int c){//找从左下角到右上角方向的棋子方法
int c1=0,c2=0;
int[] cc=new int[2];
for(int i=r-1,j=c+1;i>=0&&j<chessb.CELL_Y;i--,j++){//往右上角方向找连续相同红色的棋子个数
if(chess2[i][j]==-1){
c1++; }
else break; }
for(int i=r+1,j=c-1;j>=0&&i<chessb.CELL_X;j--,i++){//往左下角方向找连续相同 红色的棋子个数
if(chess2[i][j]==-1){
c1++; }
else break; }
for(int i=r-1,j=c+1;i>=0&&j<chessb.CELL_Y;i--,j++){//往右上角方向找连续相同黑色的棋子个数
if(chess2[i][j]==1){
c2++; }
else break; }
for(int i=r+1,j=c-1;j>=0&&i<chessb.CELL_X;j--,i++){//往左下角方向找连续相同黑色 的棋子个数
if(chess2[i][j]==1){
c2++; }
else break; }
cc[0]=c1;cc[1]=c2;
return cc; }// 返回斜向的连续相同的红色和黑色的棋子个数
}
以上的判断是为了给机器人提供信息的,返回的是当前每一空坐标点的情况。一般的方法是可以返回一个变量的,但在这里,我们需要的是通过计算,了解每空点四个方向的自己的和对方的棋子的情况。所 以为了区分开来,就要用到两个变量。我是用数组实现的,不知道能用些什么方法。
机器人得到空坐标点的信息后,就要做出分析和判断了。选出它认为最佳的空坐标的位置。然后返回给监听器。在选择最佳的位置时,这里,我是用权值来的大小来区分每一点的。看哪一点的权值最大,就认为是该点最合适了。最佳点就是机器人要下棋子的坐标,它是包含两个值的(横坐标和纵坐标)。所以又再次利用数组来返回一个坐标了。
package FiveChess;
/**
* 机器人下棋的类
* @author Administrator
*
*/
public class Robot {
int row, clow; //定义两个变量,分别指向横纵坐标
private int[][] chess2;
public int[] a = new int[2]; //定义一个数组用来返回坐标
private int[][] n = new int[4][2]; //定义一个四行两列的二维数组,行表示四个不同的方向,列表示自己和对手
pandk pd;
public Robot( int[][] chess2) {
this.chess2 = chess2; //把存权值的数组传过来
pd = new pandk(chess2); //创建判断权值的对象
}**
* 确定机器人要下的最佳坐标的方法
* @return 返回机器人要下的位置的坐标
*/
public int[] robot() {
for (int i = 0; i < chessb.CELL_Y; i++)
for (int j = 0; j < chessb.CELL_X; j++) {
if (chess2[i][j] == 0) {
n[0] = pd.count(i, j);//返回横向的自己和对手棋子的个数
n[1] = pd.count1(i, j);//返回纵向的自己和对手棋子的个数
n[2] = pd.count2(i, j);//返回y=x向的自己和对手棋子的个数
n[3] = pd.count3(i, j);//返回y=-x向的自己和对手棋子的个数
for (int k = 0; k < 4; k++) { //从四个方向分别判断
if (n[k][0] == 4) { //当自己四个任一方向有四个棋子
chess2[i][j] = chess2[i][j] + 10000000;//
} else if (n[k][1] == 4) { //当对手四个任一方向有四个棋子
chess2[i][j] = chess2[i][j] + 1000000; //常量表示权值的大小
} else if (n[k][0] == 3) { //当自己四个任一方向有三个棋子
chess2[i][j] = chess2[i][j] + 100000;
} else if (n[k][1] == 3) { //当对手四个任一方向有三个棋子
chess2[i][j] = chess2[i][j] + 10000;
} else if (n[k][0] == 2) { //当自己四个任一方向有两个棋子
chess2[i][j] = chess2[i][j] + 1000;
} else if (n[k][1] == 2) { //当对手四个任一方向有两个棋子
chess2[i][j] = chess2[i][j] + 100;
}else if(n[k][0] == 1){ //当自己四个任一方向有一个棋子
chess2[i][j] = chess2[i][j] + 10;
}else if(n[k][1] == 1){ //当对手四个任一方向有一个个棋子
chess2[i][j] = chess2[i][j] + 4; }
}
}
}
// 找数组中数值最大的坐标
int max = chess2[0][0];
for (int i = 0; i < chessb.CELL_Y; i++) {
for (int j = 0; j < chessb.CELL_X; j++) {
System.out.print(chess2[i][j] + "\t");
if (max < chess2[i][j]) {
max = chess2[i][j];
row = i;
clow = j; }
}
System.out.println();
}
//找到最大的权值后,将没有棋子的坐标上的权值清零处理
for (int i = 0; i < chessb.CELL_Y; i++) {
for (int j = 0; j < chessb.CELL_X; j++) {
if(chess2[i][j]>1) //如果该坐标没有棋子,则令它的权值为0
chess2[i][j]=0; }
}
a[0] = row; //机器人放旗子的横坐标
a[1] = clow; //机器人放旗子的纵坐标
System.out.println(row + "<><>" + clow);
return a; //返回机器人要下棋子的坐标
}
}
得到机器人要下的位置后,在监听器类中,我们就有需要做些修改了,因为整体来说,自己和机器人就下棋这点来说是一样的步骤。因此,就可以把下棋的方法单独拿出来作为一个方法,用两个的对象来调用这个方法就行了。
public void putchess(int i, int j,Color c) {
if(c.equals(Color.BLACK)){
chess[i][j] =1; // 使黑色棋子的引用值为1
chess2[i][j]=1; //同时使该坐标点的权值为1
System.out.println("--->>黑色的");}
else {
chess[i][j] = -1; //使红色棋子的引用值为-1
chess2[i][j]= -1;
System.out.println("<<<<---红色");}
tx = chessb.x0 + chessb.CELL_SIZE * j;// 获取交叉点的坐标
ty = chessb.x0 + chessb.CELL_SIZE * i;
g.setColor(c);
g.fillOval(tx- chessb.CELL_SIZE / 2, ty- chessb.CELL_SIZE / 2, chessb.CELL_SIZE,chessb.CELL_SIZE);// 画圆*/
pand pd = new pand(chess);// 创建一个判输赢的对象,并且给它传值
int n = pd.count(i, j); //
int n1 = pd.count1(i, j);
int n2 = pd.count2(i, j);
int n3 = pd.count3(i, j); // 以最后下的棋子为中心,分别从四个方向找与它颜色相同且连续的棋子的个数
if (n >= 5 || n1 >= 5 || n2 >= 5 || n3 >= 5) {// 判断是否有哪一个方向的个数达到5个了
if (chess[i][j] == 1) {// 看最高的棋子的颜色是否是黑色
JOptionPane.showMessageDialog(null, "黑色赢了!!");// 弹出黑色棋子赢了 的对话框
System.out.println(" 黑色赢了!!!");
} else {
JOptionPane.showMessageDialog(null, "红色赢了!!");// 弹出对话框
System.out.println(" 红色赢了!!!"); }
}
}