五子棋是全国智力运动会竞技项目之一,容易上手,老少皆宜,而且趣味横生,引人入胜,不仅能增强思维能力,提高智力,而且富含哲理,有助于修身养性。当然,用 JAVA 语言编写五子棋 AI 小游戏也是一件非常有意思的事:
先上效果图:
一、五子棋规则
五子棋的基本规则如下:
(1) 棋盘:采用国际上标准的 15×15 路线的正方形棋盘。
(2)下法:两人分别执黑白两色棋子,轮流在棋盘上选择一个无子的交叉点走子,无子的交叉点又称为空点,规定由黑方先行走棋。
(3)输赢判断:黑、白双方有一方的五个棋子在衡、竖或斜的方向上联接成无间断的一条线即为该方赢。
二、思路分析
(1)下五子棋首先需要一个棋盘,国际标准是 15×15 路线,为了增加趣味性,我们在编写程序时可以使用不同线路的棋盘。
(2)用户在开始游戏前可进行相关选择,如:模式选择(人人对战、人机对战),颜色选择(用户要黑棋、用户要白棋),下棋顺序(黑棋先行、白棋先行),并且设置有“开始游戏”、“重新开始”、“认输结束”、“悔棋”等按钮。
(3)开始游戏后,用户在棋盘上点击相应的位置就可以下棋子(人人模式黑白子交替出现,人机模式用户在下完一个子后电脑也会自动下一个子),且每下一步棋系统需要判断横竖斜这几个方向上是否有连续相同颜色的五个棋子。
(4)在游戏过程中,当用户改变界面大小和拖动界面,即调用 paint 函数时棋盘和棋子不会消失。
简简单单几个步骤,顿时感觉难度也不高,下面开工!let's go !!!
三、代码详解
首先,我们需要画一个棋盘,new 一个 ImageUI 类(当然也可以取其他名字),在这个类中,我们需要把五子棋的界面画出来,为了方便管理,在 JFrame 上设置了两个 JPanel,一个JPanel 里放 ImagePanel,用于棋盘和棋子的绘制,另一个JPanel 里放相关按钮。
为了能够实现模式选择等单选功能,这边使用了单选按钮 JRadioButton,并打包放入一个 group 中,由于按钮紧挨在一起不美观,我们在部分按钮后面增加了一个空的 JLabel。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
public class ImageUI extends JFrame implements Config{
ImageListener ul = new ImageListener();
ImagePanel imgPanel = new ImagePanel();
public static void main(String[] args){
new ImageUI();
}
ImageUI(){
initUI();
}
private void initUI(){
JFrame jf = frameInitUI();
JPanel[] jps = addPanel(jf);
addButton(jps,ul);
jf.setVisible(true);
Graphics gr = imgPanel.getGraphics();
ul.g = gr;
ul.imgPanel = imgPanel;
imgPanel.qz = ul.qz;
imgPanel.addMouseListener(ul);
}
private void addButton(JPanel[] jps, ActionListener al) {
JLabel j0 = new JLabel(" ");
jps[1].add(j0);
String[] btnstrs = new String[]{"开始游戏","重新开始","认输结束","悔棋一步",
" 模式 ","人人对战","人机对战"," 颜色 ","我要黑棋","我要白棋",
" 顺序 ","黑棋先行","白棋先行"};
for (int i = 0; i < 4; i++){
JButton btn = new JButton(btnstrs[i]);
Dimension dim = new Dimension(120,40);
btn.setPreferredSize(dim);
btn.setFont(new Font("黑体",1,20));
btn.setBackground(Color.WHITE);
btn.addActionListener(ul);
jps[1].add(btn);
}
JLabel j1 = new JLabel(" ");
jps[1].add(j1);
JLabel j2 = new JLabel(btnstrs[4]);
j2.setFont(new Font("楷体",1,30));
jps[1].add(j2);
ButtonGroup group1 = new ButtonGroup();
for (int i = 5; i < 7; i++){
JRadioButton jrb = new JRadioButton(btnstrs[i]);
jrb.setPreferredSize(new Dimension(120,40));
jrb.setFont(new Font("黑体",1,20));
jrb.setSelected(true);
jrb.setBackground(Color.YELLOW);
jrb.addActionListener(ul);
group1.add(jrb);
jps[1].add(jrb);
}
JLabel j3 = new JLabel(" ");
jps[1].add(j3);
JLabel j4 = new JLabel(btnstrs[7]);
j4.setFont(new Font("楷体",1,30));
jps[1].add(j4);
ButtonGroup group2 = new ButtonGroup();
for (int i = 8; i < 10; i++){
JRadioButton jrb = new JRadioButton(btnstrs[i]);
jrb.setPreferredSize(new Dimension(120,40));
jrb.setFont(new Font("黑体",1,20));
jrb.setSelected(true);
jrb.setBackground(Color.YELLOW);
jrb.addActionListener(ul);
group2.add(jrb);
jps[1].add(jrb);
}
JLabel j5 = new JLabel(" ");
jps[1].add(j5);
JLabel j6 = new JLabel(btnstrs[10]);
j6.setFont(new Font("楷体",1,30));
jps[1].add(j6);
ButtonGroup group3 = new ButtonGroup();
for (int i = 11; i < 13; i++){
JRadioButton jrb = new JRadioButton(btnstrs[i]);
jrb.setPreferredSize(new Dimension(120,40));
jrb.setFont(new Font("黑体",1,20));
jrb.setSelected(true);
jrb.setBackground(Color.YELLOW);
jrb.addActionListener(ul);
group3.add(jrb);
jps[1].add(jrb);
}
}
private JPanel[] addPanel(JFrame jf) {
JPanel[] jps = new JPanel[2];
String[] layoustrs = {BorderLayout.CENTER,BorderLayout.EAST};
imgPanel.setBackground(Color.LIGHT_GRAY);
jf.add(imgPanel,layoustrs[0]);
jps[0] = imgPanel;
Dimension dim = new Dimension(200,0);
JPanel jp_s = new JPanel();
jp_s.setBackground(Color.ORANGE);
jp_s.setPreferredSize(dim);
jf.add(jp_s,layoustrs[1]);
jps[1] = jp_s;
return jps;
}
private JFrame frameInitUI() {
setTitle("五子棋AI");
setSize(X0+SIZE*LINE+200,Y0+SIZE*LINE+SIZE);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
return this;
}
}
界面画好后,我们需要考虑下棋了,new 一个 ImageListener 类:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
public class ImageListener extends Listen implements Config{
public Graphics g = null;
private int count = 0; //偶数黑棋 奇数白棋
private int index = 0; //0无棋 1黑棋 2白棋
private int m,n; //第m行 第n列
private String name = null;
private String option = null;
public int[][] qz = new int[LINE][LINE];
public int[][] score = new int[LINE][LINE];
public int[] cc = new int[4];
public int goalX = 0;
public int goalY = 0;
public int mark = 0;
public int p = 1;
Boolean moshi = true;
Boolean yanse = true;
Boolean shunxu = true;
Boolean renji = true;
Boolean same = true;
ImagePanel imgPanel = new ImagePanel();
Login lo = new Login();
@Override
public void actionPerformed(ActionEvent e){
System.out.println("按钮被调用");
name = e.getActionCommand();
if("人人对战".equals(name)) moshi = true;
if("人机对战".equals(name)) moshi = false;
if("我要黑棋".equals(name)) yanse = true;
if("我要白棋".equals(name)) yanse = false;
if("黑棋先行".equals(name)) shunxu = true;
if("白棋先行".equals(name)) shunxu = false;
if (moshi && shunxu) {
count = 2;
}
if (moshi && !shunxu) {
count = 1;
}
if(!moshi && yanse && shunxu){ //人先出黑
count = 2;
}
if(!moshi && yanse && !shunxu){ //机先出白
count = 2;
}
if(!moshi && !yanse && shunxu){ //机先出黑
count = 1;
}
if(!moshi && !yanse && !shunxu){ //人先出白
count = 1;
}
if("认输结束".equals(name)){
option = "认输结束";
lo.showU(option);
}
if("悔棋一步".equals(name)){
cancel(qz);
}
if("开始游戏".equals(name)){
same = true;
for(int i = 0; i < 4; i++){
cc[i] = 0;
}
for(int i = 0; i < LINE; i++){
for(int j = 0; j < LINE; j++){
qz[i][j] = 0;
}
}
if(!moshi && yanse && !shunxu) {
cc[2] = LINE/2;
cc[3] = LINE/2;
qz[LINE/2][LINE/2] = 2;
}
if(!moshi && !yanse && shunxu) {
cc[2] = LINE/2;
cc[3] = LINE/2;
qz[LINE/2][LINE/2] = 1;
}
imgPanel.paint(g);
option = "开始游戏";
lo.showU(option);
System.out.println("我去调用他");
}
if("重新开始".equals(name)){
same = true;
for(int i = 0; i < LINE; i++){
for(int j = 0; j < LINE; j++){
qz[i][j] = 0;
}
}
imgPanel.paint(g);
}
}
@Override
public void mousePressed(MouseEvent e){
if(renji) {
cc[0] = cc[2];
cc[1] = cc[3];
}
}
@Override
public void mouseReleased(MouseEvent e){
System.out.println("释放被调用");
if(renji) {
int x = e.getX();
int y = e.getY();
if (count % 2 == 0) {
index = 1;
}
if (count % 2 == 1) {
index = 2;
}
if ((x > (X0 - SIZE / 2)) && (x < (X0 - SIZE / 2 + SIZE * LINE)) &&
(y > (Y0 - SIZE / 2)) && (y < (Y0 - SIZE / 2 + SIZE * LINE))) {
m = (x - X0 + SIZE / 2) / SIZE;
n = (y - Y0 + SIZE / 2) / SIZE;
if (qz[m][n] == 0) {
cc[2] = m;
cc[3] = n;
qz[m][n] = index;
count++;
imgPanel.drawqz(qz, g);
judge(qz);
System.out.println("判断被调用");
same = true;
} else {
option = "同一位置";
lo.showU(option);
same = false;
}
System.out.println("m=" + m + ";n=" + n);
}
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
if (same && p < 5) {
if (!moshi) {
renji = false;
AI(qz);
if (yanse) qz[goalX][goalY] = 2;
if (!yanse) qz[goalX][goalY] = 1;
imgPanel.drawqz(qz, g);
count++;
cc[0] = cc[2];
cc[1] = cc[3];
cc[2] = goalX;
cc[3] = goalY;
judge(qz);
}
}
}
}
private void judge(int[][] qz){
//判断|情形
for(int i = 0; i < LINE; i++){
for(int j = 0; j < LINE-1; j++){
if(qz[i][j] == qz[i][j+1] && qz[i][j] != 0){
p++;
if(p > 4 && qz[i][j] == 1) {
option = "黑棋胜";
lo.showU(option);
// same = false;
return;
}
if(p > 4 && qz[i][j] == 2) {
option = "白棋胜";
lo.showU(option);
// same = false;
return;
}
}
if(qz[i][j] != qz[i][j+1]){
p = 1;
}
}
}
//判断——情形
for(int j = 0; j < LINE; j++){
for(int i = 0; i < LINE-1; i++){
if(qz[i][j] == qz[i+1][j] && qz[i][j] != 0){
p++;
if(p > 4 && qz[i][j] == 1) {
option = "黑棋胜";
lo.showU(option);
// same = false;
}
if(p > 4 && qz[i][j] == 2) {
option = "白棋胜";
lo.showU(option);
// same = false;
return;
}
}
if(qz[i][j] != qz[i+1][j]){
p = 1;
}
}
}
//判断\情形
for(int i = 0; i < LINE-4; i++){
for(int j = 0; j < LINE-4; j++){
int m = i;
int n = j;
for(int cishu = 0; cishu < 5; cishu++) {
if (qz[m][n] == qz[m + 1][n + 1] && qz[m][n] != 0) {
p++;
m++;
n++;
if (p > 4 && qz[m][n] == 1) {
System.out.println("调用黑棋胜1");
option = "黑棋胜";
lo.showU(option);
// same = false;
return;
}
if (p > 4 && qz[m][n] == 2) {
System.out.println("调用白棋胜1");
option = "白棋胜";
lo.showU(option);
// same = false;
return;
}
}
if (qz[m][n] != qz[m + 1][n + 1]) {
p = 1;
}
}
}
}
//判断/情形
for(int i = 4; i < LINE; i++){
for(int j = 0; j < LINE-4; j++){
int m = i;
int n = j;
for(int cishu = 0; cishu < 5; cishu++) {
if (qz[m][n] == qz[m - 1][n + 1] && qz[m][n] != 0) {
p++;
m--;
n++;
if (p > 4 && qz[m][n] == 1) {
System.out.println("调用黑棋胜2");
option = "黑棋胜";
lo.showU(option);
// same = false;
return;
}
if (p > 4 && qz[m][n] == 2) {
System.out.println("调用白棋胜2");
option = "白棋胜";
lo.showU(option);
// same = false;
return;
}
}
if (qz[m][n] != qz[m - 1][n + 1]) {
p = 1;
}
}
}
}
}
private void cancel(int[][] qz){
qz[cc[0]][cc[1]] = 0;
qz[cc[2]][cc[3]] = 0;
imgPanel.paint(g);
}
private void AI(int[][] qz){
for(int i = 0; i < LINE; i++){
for(int j = 0; j < LINE; j++){
score[i][j] = 0;
}
}
int blackChessNum = 0;
int whiteChessNum = 0;
//扫描|
for(int i = 0; i < LINE; i++){
System.out.println("|被扫描了");
for(int j = 0; j < LINE-4; j++){
int k = j;
while(k < j + 5){
if(qz[i][k] == 1) blackChessNum++;
if(qz[i][k] == 2) whiteChessNum++;
k++;
}
mark = weight(blackChessNum, whiteChessNum);
for(k = j; k < j + 5; k++){
if(qz[i][k] == 0) score[i][k] += mark;
}
blackChessNum = 0;
whiteChessNum = 0;
mark = 0;
}
}
//扫描——
for(int j = 0; j < LINE; j++){
System.out.println("——被扫描了");
for(int i = 0; i < LINE-4; i++){
int k = i;
while(k < i + 5){
if(qz[k][j] == 1) blackChessNum++;
if(qz[k][j] == 2) whiteChessNum++;
k++;
}
mark = weight(blackChessNum, whiteChessNum);
for(k = i; k < i + 5; k++){
if(qz[k][j] == 0) score[k][j] += mark;
}
blackChessNum = 0;
whiteChessNum = 0;
mark = 0;
}
}
//扫描\
for(int i = 0; i < LINE-4; i++){
System.out.println("\\被扫描了");
for(int j = 0; j < LINE-4; j++){
int k = 0;
while(k < 5){
if(qz[i+k][j+k] == 1) blackChessNum++;
if(qz[i+k][j+k] == 2) whiteChessNum++;
k++;
}
mark = weight(blackChessNum, whiteChessNum);
for(k = 0; k < 5; k++){
if(qz[i+k][j+k] == 0) score[i+k][j+k] += mark;
}
blackChessNum = 0;
whiteChessNum = 0;
mark = 0;
}
}
//扫描/
for(int i = 4; i < LINE; i++){
System.out.println("/被扫描了");
for(int j = 0; j < LINE-4; j++){
int k = 0;
while(k < 5){
if(qz[i-k][j+k] == 1) blackChessNum++;
if(qz[i-k][j+k] == 2) whiteChessNum++;
k++;
}
mark = weight(blackChessNum, whiteChessNum);
for(k = 0; k < 5; k++){
if(qz[i-k][j+k] == 0) score[i-k][j+k] += mark;
}
blackChessNum = 0;
whiteChessNum = 0;
mark = 0;
}
}
//从空位置中找到得分最大的位置
int scoremax = 0;
for(int j = 0; j < LINE; j++){
System.out.println("");
for(int i = 0; i < LINE; i++){
System.out.print(score[i][j]+";");
if(scoremax < score[i][j]){
scoremax = score[i][j];
goalX = i;
goalY = j;
}
}
}
renji = true;
}
private int weight(int blackChessNum,int whiteChessNum) {
System.out.println("blackChessNum="+blackChessNum+";whiteChessNum="+whiteChessNum);
if(blackChessNum > 0 && whiteChessNum > 0) mark = 0;
if(blackChessNum == 0 && whiteChessNum == 0) mark = 7;
if(blackChessNum == 1 && whiteChessNum == 0 && yanse) mark = 15;
if(blackChessNum == 1 && whiteChessNum == 0 && !yanse) mark = 35;
if(whiteChessNum == 1 && blackChessNum == 0 && yanse) mark = 35;
if(whiteChessNum == 1 && blackChessNum == 0 && !yanse) mark = 15;
if(blackChessNum == 2 && whiteChessNum == 0 && yanse) mark = 400;
if(blackChessNum == 2 && whiteChessNum == 0 && !yanse) mark = 800;
if(whiteChessNum == 2 && blackChessNum == 0 && yanse) mark = 800;
if(whiteChessNum == 2 && blackChessNum == 0 && !yanse) mark = 400;
if(blackChessNum == 3 && whiteChessNum == 0 && yanse) mark = 1800;
if(blackChessNum == 3 && whiteChessNum == 0 && !yanse) mark = 15000;
if(whiteChessNum == 3 && blackChessNum == 0 && yanse) mark = 15000;
if(whiteChessNum == 3 && blackChessNum == 0 && !yanse) mark = 1800;
if(blackChessNum == 4 && whiteChessNum == 0 && yanse) mark = 100000;
if(blackChessNum == 4 && whiteChessNum == 0 && !yanse) mark = 800000;
if(whiteChessNum == 4 && blackChessNum == 0 && yanse) mark = 800000;
if(whiteChessNum == 4 && blackChessNum == 0 && !yanse) mark = 100000;
return mark;
}
}
ImageListener 类是五子棋 AI 代码的核心部分,下面从模式选择,胜负判断,悔棋三方面进行简要分析:
模式判断:
在人人对战中,我们用 count 计数,偶数下黑子,奇数下白子,为了实现下的第一颗棋子是黑子还是白子,可对应给 count 的初值赋 1 或 2,每下完一步棋进行胜负判断;
在人机对战模式中,主要难点是电脑自主判断落子位置,本篇文章采用了已经公开的五元组评分算法,即对15X15的572个五元组分别评分,一个五元组的得分就是该五元组为其中每个位置贡献的分数,一个位置的分数就是其所在所有五元组分数之和,所有空位置中分数最高的那个位置就是落子位置。
五子棋 AI 算法其实还有很多,这边再介绍一种也较广泛使用的算法,即根据特征来赋评估分值,同一特征可能在不同路上(甚至同一路上)会重复出现,一些威胁性小的特征即使出现多次,累加得分也不应该超过一个极有威胁性的特征得分。如特征“++OO++”威胁性小,特征“+OOOO+”威胁性非常大,不管前者出现多少个,原则上得分都不应该超过后者出现一个的得分,在此提供一个已有的特征评分表供参考:
胜负判断
在游戏过程中,我们初始化了一个和棋盘线数一样的二维数组,用于记录每一个交点的落子情况,我们规定:若该交点处没有棋子,则数组相应位置元素为0;若该交点处有一颗黑子,则数组相应位置为1;若该交点处有一颗白子,则数组相应位置为2;每下完一颗棋子,都会调用 judge 函数对该二维数组进行横向,竖向,左斜向,右斜向扫描,判断是否有连续五个1或者五个2.
悔棋
我们定义了一个长度为4的一维数组,分别记录倒数第二颗棋子的 x,y 坐标和最后一颗棋子的 x, y 坐标,当点击“悔棋一步”按钮时,把这两个棋子对应二维数组中的值置为0,并调用 paint 函数,则在重绘时这两颗棋子将不会绘制出来,即实现了悔棋功能!
当界面发生改变时,会调用 paint 函数,因此我们还写了一个 ImagePanel 类,里面重写了 paint 函数。
import javax.swing.*;
import java.awt.*;
public class ImagePanel extends JPanel implements Config{
public int[][] qz = new int[LINE][LINE];
public void paint(Graphics gr){
super.paint(gr);
drawChessTable(gr);
drawqz(qz,gr);
}
public void drawqz(int[][] qz,Graphics gr) {
System.out.println("棋子重绘");
for(int i = 0; i < LINE; i++){
for(int j = 0; j < LINE; j++){
if(qz[i][j] == 1){
System.out.println("qz["+i+"]["+j+"]="+qz[i][j]);
gr.setColor(Color.BLACK);
gr.fillOval(X0+i*SIZE-CHESS/2, Y0+j*SIZE-CHESS/2, CHESS, CHESS);
}
if(qz[i][j] == 2){
System.out.println("qz["+i+"]["+j+"]="+qz[i][j]);
gr.setColor(Color.WHITE);
gr.fillOval(X0+i*SIZE-CHESS/2, Y0+j*SIZE-CHESS/2, CHESS, CHESS);
}
}
}
}
private void drawChessTable(Graphics gr) {
Graphics2D gr2 = (Graphics2D)gr;
gr2.setStroke(new BasicStroke(2.0f));
for(int i = 0; i < LINE; i++){
gr2.drawLine(X0,SIZE*i+Y0,X0+SIZE*(LINE-1),SIZE*i+Y0);
}
for(int j = 0; j < LINE; j++){
gr2.drawLine(X0+SIZE*j,Y0,X0+SIZE*j,Y0+SIZE*(LINE-1));
}
}
}
最后,为了能使这个代码能够顺利运行,还需要加上以下几个类:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class Listen implements MouseListener, ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
public interface Config {
public static final int X0 = 50;
public static final int Y0 = 50;
public static final int SIZE = 50;
public static final int LINE = 15;
public static final int CHESS = 50;
}
public class Login {
public void showU(String option){
JFrame jf = new JFrame();
jf.setSize(500,300);
switch (option){
case"黑棋胜":
ImageIcon image1 = new ImageIcon("D:\\03.jpg");
JLabel jl1 = new JLabel(image1);
jf.add(jl1);
jf.setTitle("黑棋胜");
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
break;
case"白棋胜":
ImageIcon image2 = new ImageIcon("D:\\04.jpg");
JLabel jl2 = new JLabel(image2);
jf.add(jl2);
jf.setTitle("白棋胜");
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
break;
case"开始游戏":
ImageIcon image3 = new ImageIcon("D:\\01.jpg");
JLabel jl3 = new JLabel(image3);
jf.add(jl3);
jf.setTitle("开始游戏");
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
break;
case"认输结束":
ImageIcon image4 = new ImageIcon("D:\\02.jpg");
JLabel jl4 = new JLabel(image4);
jf.add(jl4);
jf.setTitle("认输结束");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
break;
case"同一位置":
ImageIcon image5 = new ImageIcon("D:\\05.jpg");
JLabel jl5 = new JLabel(image5);
jf.add(jl5);
jf.setTitle("无效棋");
jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
break;
}
jf.setLocationRelativeTo(null);
jf.setVisible(true);
}
}
在最后,献上代码中使用到的五张图片:
01:
02:
03:
04:
05: