Java之简单人机五子棋(一)
继上周实现了C++控制台版的五子棋之后,这周开始学习Java,顺便花了两三天时间,做出了一直想做的图形化界面的五子棋小游戏。同时在原来C++控制台程序的基础上对AI的算法进行了一定修正,修复了一些bug,并加入了悔棋的功能
C++五子棋系列传送门:
C++之简单五子棋的设计思路
C++之简单五子棋的语言设计实现
C++简单五子棋的AI设计及实现
五子棋的算法规则和类的设计在C++相关中讲过了,所以现在主要总结一下如何转成图形化界面。由于第一次接触java,顶层设计做的不是很好,还是带有浓厚的面向过程的设计风格,勉强合格吧。
效果如下
界面主要使用swing组件搭建,具体如下:
-
MyChessBoard :面板类,继承JPanel类。主要用来实现存储棋盘信息,记录下棋过程和显示棋局。考虑到各个组件都要对棋盘进行操作,所以对棋局进行记录操作的相关函数和变量均设为静态类成员。具体包括:
- 存储棋盘信息的15*15二维数组chess[][],用0代表空,1代表白,2代表黑;
- 记录最近两步走棋信息的2*2二维数组lastChess[][],用于悔棋;
- 悔棋操作函数reDo(),将lastChess数组中存储的坐标处的棋子标记为空,然后调用repaint()进行重绘;
- 标志棋盘是否为空的布尔变量isEmpty
- 负责接收坐标,更新棋盘信息的函数record(int,int);
- clearBoard()函数负责对棋盘进行清除,并初始化相关变量;
除此之外,在构造函数中对各变量进行初始化,lastChess[][]初始化为全变量-1,有棋子落子时在record函数里对其进行更新,采用队列的数据结构,存储落子的坐标。
重写父类的paintComponent用来完成棋盘和棋子的绘制。
setChess(int,int,int)函数用来接收鼠标点击的坐标和当前所下的棋子颜色,通过点击坐标计算出落子坐标,调用record函数进行记录,调用repaint函数进行重绘。
//面板,用来打印15*15的棋盘和当前在棋盘上的棋子,提供一个15*15的int数组,标记每个节点处的棋子状态
class MyChessBoard extends JPanel{
protected BufferedImage bg = null;
static public int[][] chess ;//0为空,1为白,2为黑
static protected int [][] lastChess = {{-1,-1},{-1,-1}};
static protected boolean isEmpty = true;
static public void record(int x,int y) {
lastChess[1][0] = lastChess[0][0];
lastChess[1][1] = lastChess[0][1];
lastChess[0][0] = x;
lastChess[0][1] = y;
ChessGame.reserve();
MyControlBoard.redo = false;
MyControlBoard.undo.setText("Undo");
isEmpty = false;
}
// default constructor
public MyChessBoard(){
super();
lastChess[0][0] = -1;
lastChess[0][1] = -1;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
chess = new int[15][15];
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
chess[i][j] = 0;
}
}
try {
bg = ImageIO.read(new File("D:\\administrator-\\Documents\\eclipse-workspace\\practice\\src\\Windows\\renju.png"));
}catch(Exception e) {
e.printStackTrace();
}
repaint();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
setBackground(new Color(222,184,135));
Graphics2D g1 = (Graphics2D)g;
//add background
g1.drawImage(bg, 0, 0,getWidth(),getHeight(), null);
//draw chess board
g1.setColor(Color.red);
BasicStroke seg = new BasicStroke(2,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND);
g1.setStroke(seg);
int width = getWidth();
int height = getHeight();
int vgap = height/16;
int hgap = width/16;
for(int i = 1;i<16;i++) {
g1.drawLine(hgap, i*vgap, 15*hgap, i*vgap);
g1.drawLine(i*hgap,vgap , i*hgap, 15*vgap);
}
//draw chess
int chessWidth = hgap*3/4;
int chessHeight = vgap*3/4;
for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if(chess[i][j] == 2) {
g1.setColor(Color.BLACK);
g1.fillOval((1+j)*hgap-chessWidth/2,(1+i)*vgap-chessHeight/2,chessWidth, chessHeight);
}
else{
if(chess[i][j] == 1) {
g1.setColor(Color.white);
g1.fillOval((1+j)*hgap-chessWidth/2,(1+i)*vgap-chessHeight/2,chessWidth, chessHeight);
}
}
}
}
}
//清空棋盘并重绘
static public void clearBoard() {
isEmpty = true;
lastChess[0][0] = -1;
lastChess[0][1] = -1;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
chess[i][j] = 0;
}
}
}
//悔棋
static public void reDo() {
int count = 0;
for(int i =0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if(chess[i][j] != 0)
count++;
}
}
if(count == 1) {
chess[lastChess[0][0]][lastChess[0][1]] = 0;
ChessGame.reserve();
lastChess[0][0] = -1;
lastChess[0][1] = -1;
isEmpty = true;
}
else{
if(count == 2) isEmpty = true;
chess[lastChess[0][0]][lastChess[0][1]] = 0;
chess[lastChess[1][0]][lastChess[1][1]] = 0;
lastChess[1][0] = -1;
lastChess[1][1] = -1;
lastChess[0][0] = -1;
lastChess[0][1] = -1;
}
}
//绘制一颗给定颜色和坐标的棋子
public void setChess(int x,int y,int white) {
int width = getWidth();
int height = getHeight();
int vgap = height/16;
int hgap = width/16;
int chessWidth = hgap*3/4;
int chessHeight = vgap*3/4;
for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++) {
if( x >= ((1 + j)*hgap - chessWidth/2) && x <= ((1 + j)*hgap + chessWidth/2)) {
if(y >= ((1 + i)*vgap - chessHeight/2) && y <= ((1 + i)*vgap + chessHeight/2)) {
while(chess[i][j] == 0) {
if(white == 1)
chess[i][j] = 1;
else
chess[i][j] = 2;
record(i, j);
return;
}
}
}
}
}
repaint();
}
}
- MyControlBoard类继承Jpanel类,负责绘制控制面板。同样将一些各组件都需要调用的变量设置为静态的类成员变量。如标志棋局是否开局的boolean变量started,用户执黑还是执白的标志变量whiteC,控制面板复原函数initialize()函数。MyControlBoard类中加入三个Jbutton类的对象,分别负责接收开局、认输和悔棋的用户单击输入。通过内部类ButtonAct实现接口ActionListener,来针对按钮的点击做出反应。具体反应逻辑参见代码
- -
//Control Board,控制开局,认输和投降,分别对应三个布尔变量started,redo,surrendered,并且提供int型变量whiteC标记先后手
@SuppressWarnings("serial")
class MyControlBoard extends JPanel{
protected static JButton start;
protected JButton giveUp;
protected static JButton undo;
protected JLabel b1,b2;
protected ButtonAct cbut = new ButtonAct();
static boolean started = false;
static boolean redo = false;
static boolean surrendered = false;
static int whiteC = 0; // 为0表示用户执黑先手,为1表示用户执白后走
//constructor
public MyControlBoard(){
super();
this.setBackground(Color.darkGray);
GridLayout bl = new GridLayout(5,1);
this.setLayout(bl);
b1 = new JLabel();
b2 = new JLabel();
start = new JButton("Start");
start.setBackground(Color.yellow);
start.setSize(getWidth(), getHeight()/10);
start.addActionListener(cbut);
giveUp = new JButton("Give up");
giveUp.setBackground(Color.lightGray);
giveUp.setSize(getWidth(), getHeight()/10);
giveUp.addActionListener(cbut);
undo = new JButton("Undo");
undo.setBackground(Color.cyan);
undo.setSize(getWidth(), getHeight()*6/10);
undo.addActionListener(cbut);
add(start);
add(b1);
add(giveUp);
add(b2);
add(undo);
}
//复原为初始状态
static public void initialize() {
started = false;
redo = false;
surrendered = false;
whiteC = 0;
start.setText("Start");
undo.setText("Undo");
}
private class ButtonAct implements ActionListener {
public void actionPerformed(ActionEvent e) {
// TODO 自动生成的方法存根
if(e.getActionCommand().equals("Start")) {
start.setText("Started");
started = true;
whiteC = JOptionPane.showConfirmDialog(start, "是否执黑先行?(若选择执白单击棋盘后棋局开始)", "先后手确认", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
}
if(e.getActionCommand().equals("Undo")) {
if(started && !MyChessBoard.isEmpty) {
redo = true;
undo.setText("Undone");
MyChessBoard.reDo();
}
}
if(e.getActionCommand().equals("Give up")) {
if(started) {
JOptionPane.showMessageDialog(giveUp, "你已认输!","结果",JOptionPane.INFORMATION_MESSAGE);
surrendered = true;
started = false;
start.setText("Start");
MyChessBoard.clearBoard();
initialize();
ChessGame.takeTurn = 0;
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
}
}
}
}
}
其实这里有一些设计上的失误,因为之后的棋盘事件侦听需要综合棋盘点击和控制面板点击进行判定先后手,所以其实将MyControlBoard类设计为MyChessBoad类的内部类比较好,可以通过设置布局管理器为BorderLayout来实现添加子面板。
接下来设计实现ChessGame类,将各个组件组合在一起,实现下棋流程。并在ChessGame类中实现MyChessBoad类的事件侦听程序。
ChessGame类的静态变量主要包括标记当前轮到哪方落子的交替标志量takeTurn和对其进行反转的函数rserve();record函数每一次记录的时候都调用reserve函数反转标志量,从而使得每次会接受一次鼠标点击。
在ChessGame中有用来检查是否有连成五个的CheckFive函数和用来判断局势的judge函数。并定义内部类ClickAct继承MouseAdapter适配器,用来对鼠标点击做出反应。在该类中定义一个初始化为真的布尔变量goAI,用来区分先后手,每次鼠标点击之后,先进行先后手检测,当棋局已开局、棋盘为空且用户棋子颜色与反转标志量不一致,则调用AI进行走棋,同时goAI值置为假,当goAI值为真时,则先接收用户点击,计算出点击坐标对应的棋子坐标,调用setChess函数,再调用judge函数判断局势,如果没有获胜方,再调用AI下棋,再进行局势判断,再根据局势弹出相应对话框或者什么都不做。
代码如下:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class ChessGame {
JFrame frame;
MyChessBoard chessBoard;//棋盘面板
MyControlBoard control;//控制面板
static int takeTurn = 0;//交替标志量,记录当前轮到哪方落子
//反转交替标志量
static public void reserve() {
if(takeTurn == 0)
takeTurn = 1;
else
takeTurn = 0;
}
public ChessGame() {
frame =new JFrame("五子棋");
frame.setSize(850, 710);
control = new MyControlBoard();
frame.getContentPane().add(control,BorderLayout.EAST);
control.setVisible(true);
chessBoard = new MyChessBoard();
}
public void play() {
ClickAct playChess = new ClickAct();
frame.getContentPane().add(chessBoard,BorderLayout.CENTER);
chessBoard.repaint();
chessBoard.setVisible(true);
chessBoard.addMouseListener(playChess);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//判断棋局形式,返回-1继续,0表示平局,1表示白棋获胜,2表示黑棋获胜
private int judge() {
boolean full = true;
if(checkFive() >= 5) {
return MyChessBoard.chess[MyChessBoard.lastChess[0][0]][MyChessBoard.lastChess[0][1]];
}
loop: for(int i = 0;i < 15;i++) {
for(int j = 0;j < 15;j++)
{
if(MyChessBoard.chess[i][j] == 0){
full = false;
break loop;
}
}
}
if(full) {
return 0;
}
else {
return -1;
}
}
//检查连子情况
private int checkFive()
{
int x = MyChessBoard.lastChess[0][0], y = MyChessBoard.lastChess[0][1];
if(x <= 0 || x >= 15)
return 1;
if(y <= 0 || y >= 15)
return 1;
int bd[][] = MyChessBoard.chess;
if(MyChessBoard.isEmpty)
return 0;
int count = 1;//计数器,统计同色个数
int sum[] = {0,0,0,0};
boolean locked = false;//逻辑标记量,用来标记是否遇到了非同色节点
//水平方向检测
for(int i = 1;i < 5 && ((x - i) >= 0) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
{
if(bd[x][y] == bd[x - i][y]) {
count++;
}
else
locked = true;
}
locked = false;
for(int i = 1;i < 5&&((x + i) <= 14) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x + i][y])
count++;
else
locked = true;
sum[0]=count;
if(count >= 5)
return count;
//竖直方向检测
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y - i) >= 0) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x][y - i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((y + i) <= 14) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x][y + i])
count++;
else
locked = true;
sum[1]=count;
if(count>=5)
return count;
//左上到右下斜向检测
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y - i) >= 0) && ((x - i) >= 0) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x - i][y - i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((x + i) <= 14) && ((y + i) <= 14) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x + i][y + i])
count++;
else
locked = true;
sum[2] = count;
if(count >= 5)
return count;
//左下到右上斜向检测
count = 1;
locked = false;
for(int i = 1;i < 5 && ((y + i) <= 14) && ((x - i) >= 0) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x - i][y + i])
count++;
else
locked = true;
locked = false;
for(int i = 1;i < 5 && ((x + i) <= 14) && ((y - i) >= 0) && (!locked);i++)//终止循环条件:同色超过4个或触碰到棋盘边界或遇到非同色节点
if(bd[x][y] == bd[x + i][y - i])
count++;
else
locked = true;
sum[3] = count;
if(count >= 5)
return count;
return MAX(sum,4);
}
//求最值
private int MAX(int[] a, int n) {
int max = a[0];
for(int i =1; i < n ;i++)
{
if(a[i] > max)
max = a[i];
}
return max;
}
//棋盘的事件响应类
protected class ClickAct extends MouseAdapter{
public void mousePressed(MouseEvent e) {
int x = e.getX();
int y = e.getY();
int state = judge();
AI ai = new AI();
boolean goAI = true;
if(MyControlBoard.started && MyControlBoard.whiteC == 1 && MyChessBoard.isEmpty) {
ai.play();
chessBoard.repaint();
goAI = false;
}
if(MyControlBoard.started && (takeTurn == MyControlBoard.whiteC)&&goAI)
{
chessBoard.setChess(x, y, MyControlBoard.whiteC);
state = judge();
if((state == -1) && (takeTurn != MyControlBoard.whiteC)&&goAI) {
ai.play();
chessBoard.repaint();
state = judge();
// System.out.println("state="+state);
}
switch(state) {
case 1:
if(MyControlBoard.whiteC == 1)
JOptionPane.showMessageDialog(frame, "恭喜:你赢了!","结果",JOptionPane.INFORMATION_MESSAGE);
else
JOptionPane.showMessageDialog(frame, "抱歉:你输了!","结果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
case 2:
if(MyControlBoard.whiteC == 0)
JOptionPane.showMessageDialog(frame, "恭喜:你赢了!","结果",JOptionPane.INFORMATION_MESSAGE);
else
JOptionPane.showMessageDialog(frame, "抱歉:你输了!","结果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
case 0:
JOptionPane.showMessageDialog(frame, "平局!","结果",JOptionPane.INFORMATION_MESSAGE);
MyControlBoard.surrendered = true;
MyControlBoard.started = false;
MyControlBoard.start.setText("Start");
MyChessBoard.clearBoard();
MyControlBoard.initialize();
MyChessBoard.lastChess[1][0] = -1;
MyChessBoard.lastChess[1][1] = -1;
MyChessBoard.lastChess[0][0] = -1;
MyChessBoard.lastChess[0][1] = -1;
takeTurn = 0;
break;
default:
break;
}
}
}
}
}
AI类和main类实现下篇继续
上一篇: 人机五子棋实现原理
下一篇: C++实现简单五子棋游戏
推荐阅读
-
JAVA设计模式之简单粗暴学建造者模式
-
JAVA WEB快速入门之从编写一个基于SpringBoot+Mybatis快速创建的REST API项目了解SpringBoot、SpringMVC REST API、Mybatis等相关知识
-
Java入门(一)——编写一个简单的Java程序
-
Java基础之IO技术(一)
-
JAVA WEB快速入门之从编写一个基于SpringMVC框架的网站了解Maven、SpringMVC、SpringJDBC
-
一文看懂JAVA设计模式之工厂模式
-
Java控制台版五子棋的简单实现方法
-
【每日一道算法题】Leetcode之longest-increasing-path-in-a-matrix矩阵中的最长递增路径问题 Java dfs+记忆化
-
什么是递归?用Java写一个简单的递归程序
-
Java Socket聊天室编程(一)之利用socket实现聊天之消息推送