我们分3个大步骤来实现:
1. 在窗体上画棋盘、画棋子(棋子在网格交点、交替落子)
2. 判断输赢,即寻找是否在同一直线上有5子相邻
3. 人机大战
前2个步骤就可以实现基础的人人对战,让我们开始吧
1.在窗体上画棋盘、画棋子首先,我们需要一个窗体,设置好名称,合适的大小,添加鼠标的监听器,之后设置其可见。
然后再画棋盘,我们可以根据棋盘的行列数、间隔,算出棋盘大小。你想让棋盘显示在什么位置,就算好每一根线的起止点的坐标,使用g.drawRect将其画出就好了。当然,一根根画肯定很麻烦,也可以使用循环来添加所有的线(将在源码中演示这种做法)。
最后实现绘制棋子。我们使用g.fillOval很容易的实现在鼠标按下位置绘制棋子。但是绘制的棋子怎么在交点上,还有在后面判断输赢中和重绘中都需要再次使用棋子坐标该怎么办呢。
在此我们将整个棋盘的交点集,看成一个二维数组或者矩阵。每下一颗棋,就将棋子的颜色信息,保存到矩阵对应的位置中去。之后再将矩阵中的每一个棋子画在棋盘上。
还有一些小细节,比如黑白棋子要交替落子,不能在同个地方重复落子等,一起在代码中讲解吧。
public class FiveChessframe extends Jframe implements MouseListener {
//棋子坐标
int x = 0;
int y = 0;
//演示棋盘为16*16,所以需要16*16的二维数组来保存所有棋子的颜色信息
//0表示没有棋子,1表示黑子,2表示白子,默认为0没有棋子
int[][] allChess = new int [16][16];
//标识下一步黑白棋,black or white,true为黑棋
boolean b_w = true ;
//设置窗体
public FiveChessframe() {
this.setTitle("五子棋");
this.setSize(1600,1400);
this.setLocationRelativeTo(null);
this.setResizable(false);
this.setDefaultCloseOperation(Jframe.EXIT_ON_CLOSE);
this.addMouseListener(this);
this.setVisible(true);
}
//重写Jframe中的paint方法
public void paint(Graphics g) {
super.paint;//调用Jframe中的paint方法,继承原有功能
Graphics2D g2d=(Graphics2D)g;将画笔转换为2D,可以设置划线粗细
//使用for循环绘制棋盘,根据窗体的大小设置棋盘大小
//演示中棋盘左上角x,y为275,175,右下角1325,1225,间距70
for(int i = 0;i<16;i++) {
g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(3));//调整画笔粗细
g2d.drawLine(275, 175+70*i, 1325, 175+70*i);
g2d.drawLine(275+70*i, 175, 275+70*i, 1225);
}
//画棋子的部分建议先看下文鼠标按下如何保存坐标
//根据保存在二维数组中的棋子颜色信息,画出棋子
//使用for循环,遍历二维数组中的每一个数的颜色信息
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
if (allChess[i][j]== 1) {
//根据数组保存坐标时相反的运算,算出棋子在棋盘上的坐标
int X = i * 70 + 275;
int Y = j * 70 + 175;
g2.fillOval(X-30, Y-30, 60, 60);//椭圆工具以x y 坐标为原点绘制,如果想使绘制的图形出现在鼠标点击的地方,x需要偏移半个宽度,y需要偏移半个高度
}
if (allChess[i][j]== 2) {
int X = i * 70 + 275;
int Y = j * 70 + 175;
g2.setColor(Color.WHITE);
g2.fillOval(X-30, Y-30, 60, 60);//白色棋子为白底黑边
g2.setColor(Color.BLACK);
g2.drawOval(X-30, Y-30, 60, 60);
}
}
}
}
public void mouseClicked (MouseEvent e) {
}
public void mousePressed (MouseEvent e) {
System.out.println("X "+e.getX());
System.out.println("Y "+e.getY());
//鼠标按下获取xy坐标
x = e.getX();
y = e.getY();
//限制仅在棋盘上按下有效
if (x >= 275 && x <= 1325 && y >= 175 && y <= 1225 ) {
//在数组中x坐标 = (x按下坐标-左边与窗体的距离)/ 线间隔
//得到的小数会自动转成整数,对应棋盘图中0,1,2……
//因为小数部分会被截断,而不会四舍五入,所以x按下坐标再向右偏移半个线间隔
//x坐标 = (x按下坐标+半个线间隔-左边与窗体的距离)/ 线间隔
x = (x - 240) / 70;
y = (y - 140) / 70;
//先判断此处有没有棋子
if(allChess[x][y]== 0) {
//判断棋子颜色 如果是黑方在落子,就在对应数组的xy坐标上保存颜色信息1
if(b_w == true) {
allChess[x][y] = 1;
//然后改变为白方落子,实现双方交替落子
b_w = false;
}else {
allChess[x][y] = 2;
b_w = true;
}
}
}
this.repaint();//每次落子后,需要调用重绘,将所有棋子画在棋盘上。
}
小结 第一步的核心思想:先将棋子颜色信息保存在二维数组对应的坐标中,再遍历数组,将所有的棋子绘制出。
2.判断输赢五子棋的判断输赢很简单,就是判断在一条线上有么有相邻的5个同颜色的棋子
如图所示,每个棋子都有横、竖、左上-右下、右上-左下四个方向连成一条线的可能。
对于每个方向都要判断,判断代码如下:
private boolean win(){
boolean flag = false;
int color = allChess[x][y];
//横向判断
int count = 1;//相邻棋子数量
int i = 1;
while (color == allChess[x+i][y]){
count ++;
i ++;
}
i = 1;
while (color == allChess[x-i][y]){//横向左右两边都要算
count ++;
i ++;
}
if(count >= 5){
flag = true;
}
//纵向判断
int count2 = 1;
int i2 = 1;
while (color == allChess[x][y+i2]){
count2 ++;
i2 ++;
}
i2 = 1;
while (color == allChess[x][y-i2]){
count2 ++;
i2 ++;
}
if(count2 >= 5){
flag = true;
}
//右上到左下判断
int count3 = 1;
int i3 = 1;
while (color == allChess[x + i3][y - i3]){
count3 ++;
i3 ++;
}
i3 = 1;
while (color == allChess[x - 3i][y + 3i]){
count3 ++;
i3 ++;
}
if(count3 >= 5){
flag = true;
}
//左上到右下判断
int count4 = 1;
int i4 = 1;
while (color == allChess[x + i4][y + i4]){
count4 ++;
i4 ++;
}
i4 = 1;
while (color == allChess[x - 4i][y - 4i]){
count4 ++;
i4 ++;
}
if(count4 >= 5){
flag = true;
}
return flag;
}
关键是什么时候执行这个判断呢,遍历数组中的每一个数吗?
其实只要在每一次落子之后,判断这个位置是否胜利就可以了。在每一次落子后调用win方法。
整体源码会粘在最后,先看如何实现人机对战吧
3.人机对战五子棋的战术套路有很多,但是有一个必要的步骤就是:
计算棋盘上每一个空位的胜率,取胜率最高的空位落子。
如何计算胜率的方法就有很多了,我介绍一种简单有效的。
如图所示,黄色点代表一个空位,深蓝色1号框代表和这个空位连成五颗棋子的一种可能。在这个可能中:
既有机器落子,又有人类落子 + 0 分;
全空,没有落子 + 7 分;
有机器的 1 子 + 35 分;
有机器的 2子 + 800 分;
有机器的 3 子 + 15000 分;
有机器的 4 子 + 800000 分;
有人类的 1 子 + 15 分;
有人类的 2 子 + 400 分;
有人类的 3 子 + 1800 分;
有人类的 4 子 + 100000 分;
这样我们可以得到这一种可能性的分值。
如图所示,一个空位的1个方向,有5种五子连珠的可能性。一共有4个方向就是20种可能性。将这20种可能性的分值相加,就得到了这个空位的总分。然后遍历整个棋盘上的空位,找出总分最大的空位落子。
可能会有人问“为什么有人类棋子的时候也要加分,不是应该机器的棋子越多越好吗?”,这是因为机器需要防守,防止人类获胜,当人类的棋子数量多的时候,也应该提升分数,用来防守。所以说具体的得分是很重要的,本文中的得分是借鉴其他作者的,尝试了很多次后得出。
我们将所有部分的代码整合一下,又添加了一些棋盘的基础功能,比如开始游戏、认输、退出等等,也加入了一些防止频闪的细节功能,都在代码中注释出了。发在最后。
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.Jframe;
import javax.swing.JOptionPane;
public class FiveChessframe extends Jframe implements MouseListener {
int x = 0;
int y = 0;
int[][] allChess = new int [16][16];
int[][] score = new int [16][16];//保存每个空位得分的二维数组
//标识下一步黑白棋
boolean b_w = true ;
//标识游戏结束
boolean start = true ;
//保存显示的提示信息
String message = "人类先行";
public FiveChessframe() {
this.setTitle("五子棋");
this.setSize(1600,1400);
this.setLocationRelativeTo(null);
this.setResizable(false);
this.setDefaultCloseOperation(Jframe.EXIT_ON_CLOSE);
this.addMouseListener(this);
this.setVisible(true);
//刷新屏幕,防止刚开始无法显示
this.repaint();
}
public void paint(Graphics g) {
//双缓冲技术,防止屏幕闪烁
BufferedImage bi = new BufferedImage(1600,1400,BufferedImage.TYPE_INT_ARGB);
Graphics g2 = bi.createGraphics();
Graphics2D g2d=(Graphics2D)g2;
//绘制背景
g2d.setColor(Color.BLACK);
g2.setFont(new Font("黑体",Font.BOLD,70));
g2.drawString("轮到谁了?"+message, 550, 150);
g2.drawString("开始游戏", 276, 1300);
g2.drawString("认输", 750, 1300);
g2.drawString("退出", 1153, 1300);
g2d.setColor(Color.ORANGE);
g2d.setStroke(new BasicStroke(3));
g2.drawRect(274, 1237, 300, 80);
g2.drawRect(749, 1237, 150, 80);
g2.drawRect(1150, 1237, 150, 80);
//绘制棋盘
for(int i = 0;i<16;i++) {
g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(3));
g2d.drawLine(275, 175+70*i, 1325, 175+70*i);
g2d.drawLine(275+70*i, 175, 275+70*i, 1225);
}
//标注点位
g2.fillOval(480, 378, 12, 12);
g2.fillOval(1109, 1011, 12, 12);
g2.fillOval(1109, 378, 12, 12);
g2.fillOval(480, 1009, 12, 12);
//绘制棋子
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
if (allChess[i][j]== 1) {
int X = i * 70 + 275;
int Y = j * 70 + 175;
g2.fillOval(X-30, Y-30, 60, 60);
}
if (allChess[i][j]== 2) {
int X = i * 70 + 275;
int Y = j * 70 + 175;
g2.setColor(Color.WHITE);
g2.fillOval(X-30, Y-30, 60, 60);
g2.setColor(Color.BLACK);
g2.drawOval(X-30, Y-30, 60, 60);
}
}
}
g.drawImage(bi,0,0,this);
}
public void mouseClicked (MouseEvent e) {
}
public void mousePressed (MouseEvent e) {
System.out.println("X "+e.getX());
System.out.println("Y "+e.getY());
x = e.getX();
y = e.getY();
if(start == true ) {
if (x >= 275 && x <= 1325 && y >= 175 && y <= 1225 ) {
x = (x - 240) / 70;
y = (y - 140) / 70;
//判断棋子颜色
if(allChess[x][y]== 0) {
if(b_w == true) {
allChess[x][y] = 1;
//判断胜负
boolean winFlag = this.win();
if(winFlag == true) {
JOptionPane.showMessageDialog(this, "游戏结束:人类获胜" );
start = false ;
message = "机器!";
}else {
int[] arr2 = this.AI();
x = arr2[0];
y = arr2[1];
allChess[x][y] = 2;
//判断胜负
boolean winFlag2 = this.win();
if(winFlag2 == true) {
JOptionPane.showMessageDialog(this, "游戏结束:机器获胜");
start = false ;
}
message = "人类!";
}
}
}
this.repaint();
}
}
//设置 重新开始游戏 按钮
if(x>273&&x<573&&y>1238&&y<1319) {
int result = JOptionPane.showConfirmDialog(this, "确认重新开始游戏!");
if(result == 0) {
for(int i = 0; i < 16;i++) {
for(int j = 0; j < 16;j++) {
allChess[i][j] = 0;
}
}
message = "黑方先行";
b_w = true;
start = true;
this.repaint();
}
}
//设置 认输 按钮
if(x>749&&x<899&&y>1238&&y<1319) {
int result = JOptionPane.showConfirmDialog(this, "确认认输!");
if (result == 0) {
if(b_w) {
JOptionPane.showMessageDialog(this, "黑方认输,白方获胜!游戏结束");
}else {
JOptionPane.showMessageDialog(this, "白方认输,白方获胜!游戏结束");
}
start = false;
}
}
//设置 退出游戏 按钮
if(x>1150&&x<1300&&y>1238&&y<1319) {
int result = JOptionPane.showConfirmDialog(this, "确认退出!");
if(result == 0) {
JOptionPane.showMessageDialog(this, "游戏结束");
System.exit(0);
}
}
}
public void mouseReleased (MouseEvent e) {
}
public void mouseEntered (MouseEvent e) {
}
public void mouseExited (MouseEvent e) {
}
//判断胜负方法,和上文思路相同,但整合了判断,重复的代码更少,更精简。
private boolean win() {
boolean flag = false;
int count = 1 ;
int color = allChess[x][y];
count = this.count(1, 0, color);
if (count >= 5) {
flag = true;
}else {
count = this.count(0, 1, color);
if (count >= 5) {
flag = true;
}else {
count = this.count(1, -1, color);
if (count >= 5) {
flag = true;
}else {
count = this.count(1, 1, color);
if (count >= 5) {
flag = true;
}
}
}
}
return flag;
}
//判断棋子链接数量
private int count(int xChange,int yChange,int color) {
int count = 1;
int temptX = xChange;
int temptY = yChange;
while (x+xChange>=0&&x+xChange<=15&&y+yChange>=0&&y+yChange<=15&&color == allChess[x+xChange][y+yChange]) {
count ++;
if(xChange!=0) {
xChange++;
}
if(yChange!=0) {
if(yChange > 0) {
yChange++;
}else {
yChange--;
}
}
}
xChange = temptX;
yChange = temptY;
while (x-xChange>=0&&x-xChange<=15&&y-yChange>=0&&y-yChange<=15&&color == allChess[x-xChange][y-yChange]) {
count ++;
if(xChange!=0) {
xChange++;
}
if(yChange!=0) {
if(yChange > 0) {
yChange++;
}else {
yChange--;
}
}
}
xChange = temptX;
yChange = temptY;
return count ;
}
public int[] AI() {
for(int i = 0; i < 16;i++) {
for(int j = 0;j < 16;j++) {
score[i][j] = 0;
}
}
int humanChessmanNum = 0;//人类棋子数量
int machineChessmanNum = 0;//机器棋子数量
int tupleScoreTmp = 0;//暂时得分
int goalX = -1;//最大得分x坐标
int goalY = -1;//最大得分y坐标
int maxScore = -1;//最大总分
for(int i = 0; i < 16; i++) {
for(int j = 0; j <12; j++) {
int k = j;
while(k < j + 5) {
if(allChess[i][k] == 2) {
machineChessmanNum++;
}else if(allChess[i][k] == 1) {
humanChessmanNum++;
}
k++;
}
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(k = j; k < j + 5; k++) {
score[i][k] += tupleScoreTmp;
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
for(int i = 0; i < 16; i++) {
for(int j = 0; j <12; j++) {
int k = j;
while(k < j + 5) {
if(allChess[k][i] == 2) {
machineChessmanNum++;
}else if(allChess[k][i] == 1) {
humanChessmanNum++;
}
k++;
}
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(k = j; k < j + 5; k++) {
score[k][i] += tupleScoreTmp;
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
//右上到左下的上册
for(int i = 14;i >= 4;i--) {
for(int k = i, j = 0; j < 16 && k >= 0; j++, k--) {
int m = k;
int n = j;
while(m > k - 5 && k - 5 >= -1) {
if(allChess[m][n] == 2) {
machineChessmanNum++;
}else if(allChess[m][n] == 1) {
humanChessmanNum++;
}
m--;
n++;
}
if(m == k - 5) {
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(m = k, n = j; m > k - 5 ; m--, n++) {
score[m][n] += tupleScoreTmp;
}
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
//右上到左下的下册
for(int i = 14;i >= 4;i--) {
for(int k = i, j = 0; j < 16 && k >= 0; j++, k--) {
int m = k;
int n = j;
while(m > k - 5 && k - 5 >= -1) {
if(allChess[n][m] == 2) {
machineChessmanNum++;
}else if(allChess[n][m] == 1) {
humanChessmanNum++;
}
m--;
n++;
}
if(m == k - 5) {
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(m = k, n = j; m > k - 5 ; m--, n++) {
score[n][m] += tupleScoreTmp;
}
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
//左上到右下的上册
for(int i = 0;i < 11;i++) {
for(int k = i, j = 0; j < 16 && k < 16; j++, k++) {
int m = k;
int n = j;
while(m < k + 5 && k + 5 <= 15) {
if(allChess[m][n] == 2) {
machineChessmanNum++;
}else if(allChess[m][n] == 1) {
humanChessmanNum++;
}
m++;
n++;
}
if(m == k + 5) {
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(m = k, n = j; m < k + 5 ; m++, n++) {
score[m][n] += tupleScoreTmp;
}
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
//左上到右下的下册
for(int i = 0;i < 11;i++) {
for(int k = i, j = 0; j < 16 && k < 16; j++, k++) {
int m = k;
int n = j;
while(m < k + 5 && k + 5 <= 15) {
if(allChess[n][m] == 2) {
machineChessmanNum++;
}else if(allChess[n][m] == 1) {
humanChessmanNum++;
}
m++;
n++;
}
if(m == k + 5) {
tupleScoreTmp = tupleScore(humanChessmanNum,machineChessmanNum);
for(m = k, n = j; m < k + 5 ; m++, n++) {
score[n][m] += tupleScoreTmp;
}
}
humanChessmanNum = 0;
machineChessmanNum = 0;
tupleScoreTmp = 0;
}
}
int[] arr1 = new int[2];
//从空位置找到得分最大位置
for(int i = 0; i < 16; i++) {
for(int j = 0; j < 16; j++){
if(allChess[i][j] == 0 && score[i][j] > maxScore) {
goalX=i;
goalY=j;
maxScore = score[i][j];
arr1[0] = goalX;
arr1[1] = goalY;
}
}
}
return (arr1);
}
public int tupleScore(int humanChessmanNum,int machineChessmanNum) {
//既有人类落子,又有机器落子,为0分
if(humanChessmanNum > 0 && machineChessmanNum > 0) {
return 0;
}
//全部为空没有落子,为7分
if(humanChessmanNum == 0 && machineChessmanNum == 0) {
return 7;
}
//机器落1子,35分
if(machineChessmanNum == 1) {
return 35;
}
//机器落2子,800分
if(machineChessmanNum == 2) {
return 800;
}
//机器落3子,15000分
if(machineChessmanNum == 3) {
return 15000;
}
//机器落4子,800000分
if(machineChessmanNum == 4) {
return 800000;
}
//人类落1子,15分
if(humanChessmanNum == 1) {
return 15;
}
//人类落2子,400分
if(humanChessmanNum == 2) {
return 400;
}
//人类落3子,1800分
if(humanChessmanNum == 3) {
return 1800;
}
//人类落4子,100000分
if(humanChessmanNum == 4) {
return 100000;
}
return -1;
}
}
public class MyChessframe {
public static void main(String[] args) {
FiveChessframe fcf = new FiveChessframe();
}
}
大功告成,此演示代码还有很多可以优化的地方,五子棋游戏代表棋类游戏也还有很多有意思的功能可以添加,例如:下棋计时、悔棋、复盘啦等等。快动手试试吧!



