一个简单的JAVA小项目,主要实现的功能有:
一、下棋功能,在棋盘的交点处落子。
二、悔棋功能,取消最后一颗下的棋子。
三、简单人机对战功能。
1.窗体实现。主要使用了JFrame类
public class GameUI {
public static void main(String[] args) {
GameUI ui = new GameUI();
ui.showUI();
}
//显示游戏界面
public void showUI() {
//窗体
JFrame jf = new JFrame();
//自定义的面板和鼠标监听器实现类
GamePanel gp = new GamePanel(jf);
GameMouse mouse = new GameMouse();
//将面板添加进窗体
jf.add(gp);
//像素点>分辨率,设置窗体大小
jf.setSize(800, 800);
jf.setTitle("五子棋游戏");
jf.getContentPane().setBackground(Color.pink);
//居中显示
jf.setLocationRelativeTo(null);
//设置退出进程
jf.setDefaultCloseOperation(3);
//设置可见
jf.setVisible(true);
//从面板上获取画笔,一定要在窗体显示可见之后
Graphics g = gp.getGraphics();
//绑定事件处理类
mouse.setG(g);
mouse.setGP(gp);
gp.setGM(mouse);
//给面板添加鼠标监听器方法
gp.addMouseListener(mouse);
}
}
2.绘制棋盘和棋子。
定义一个画板类对象,基础结构如下,其中定义了一个二维数组,用于存储每个位置棋子的情况。
public class GamePanel extends JPanel implements ActionListener,ChessConfig {
private JFrame jf ;
private int[][] map = new int[11][11];
public GameMouse gm;
public int[][] getMap(){
return map;
}
public int getChess(int[][]map,int x,int y){
return map[x][y];
}
public void setGM(GameMouse gm){this.gm=gm;}
public void setMap(int x,int y,int z){
map[x][y]=z;
}
//构造方法
public GamePanel(JFrame jf) {
this.jf = jf;
this.setOpaque(false);
createMenu();
}
//重写paint方法,其目的是在每次需要重绘时(如窗口最小化)保持相同的情况
@Override
public void paint(Graphics g) {
super.paint(g);
drawGrid(g);
drawChess(g);
g.setColor(Color.PINK); //用于保持落子顺序指示器
g.fillRect(680,25,100,50);
g.setColor(Color.black);
g.setFont(createFont());
g.drawString("当前:"+gm.message,680,50);
}
//获取字体
public Font createFont(){
return new Font(LETTER,Font.BOLD,LETTER_SIZE);
}
}
2.1 绘制棋盘和棋子。
绘制棋盘:此处有多种方法,我采用的是循环画正方形的方法来绘制棋盘:
public void drawGrid(Graphics g){
for (int i = 0; i < SQUARE_NUM; i++) {
for (int j = 0; j <SQUARE_NUM ; j++) {
g.drawRect((X0+SQUARE*j),(Y0+SQUARE*i),SQUARE,SQUARE);
}
}
}
绘制棋子:我采用的是Graphics类自带的fillOval方法,以黑棋为例:
public void drawBlack(Graphics g,int x,int y ){
g.setColor(Color.BLACK);
g.fillOval(x*SQUARE+X0-CHESS/2,y*SQUARE+Y0-CHESS/2,CHESS,CHESS);
}
关于fillOval方法:fillOval(int x,int y ,int width ,int height)
前两个参数x和y是椭圆外接正切矩形的左上角顶点,width和height代表了矩形的宽和高,在画圆时要注意坐标的处理,使得棋子的圆心落在十字线交叉处。
此外要注意画笔获取的先后顺序,Graphics类方法调用时出现的空指针异常。
2.2 重写actionPerformed方法。
根据对应指令实现不同操作,以认输为例:
if(command.equals("surrender")){
JOptionPane.showMessageDialog(null, gm.message + "认输");
newGame();
repaint();
}
repaint()的目的是手动重新绘制所有组件。
3.实现落子。
落子的实现主要依靠鼠标监听器的实现类,关键要素有:鼠标点击处的坐标与棋子坐标的转换,防止在同一处重复落子,黑白交替,输赢判断。
public class GameMouse implements MouseListener,ChessConfig {
private Graphics g; //定义变量,保存传递过来的画笔对象
private JFrame jf;
public GamePanel gp;
public int turn; //指示黑白顺序
public int flag ; //指示获胜方
public int xc ,yc; //棋子的交点值
public boolean start; //游戏开始判断
public String message; //落子顺序判断
public Stack<Integer> stckx; //用于保存坐标 实现悔棋功能
public Stack<Integer> stcky;
public boolean AI;//人机对战判断
ChessAI ai = new ChessAI();
//set方法,初始化棋盘和画笔
public void setG(Graphics g) {
this.g = g;
}
public void setGP(GamePanel gp){
this.gp = gp;
}
public GameMouse(){
this.stckx=new Stack<>();
this.stcky=new Stack<>();
this.turn=1;
this.flag=0;
this.start=true;
this.message="";
this.AI= false;
}
3.1 棋子坐标处理
以x坐标为例:
public void mouseClicked(MouseEvent e) {
//获取当前坐标值
int x = e.getX();
//超出棋盘的点落在边界点上
if(x>XX){
x=XX;
}
if(x<X0){
x=X0;
}
//计算棋子坐标
xc = positionX(x);
//计算棋子位置,在判定范围内落子都在同一处
public int positionX(int s){
int res=0;
if((s-X0)%SQUARE<=SQUARE/2){
res = (s-X0)/SQUARE;
}if((s-X0)%SQUARE>SQUARE/2){
res = (s-X0)/SQUARE+1;
}
return res;
}
//判断是否已经有棋子
public boolean isFree(int x,int y){
int [][] map = gp.getMap();
return gp.getChess(map, x, y) == 0;
}
3.2 落子功能
依靠每次绘制棋子后改变对应变量值来实现黑白交替。
//绘制棋子
if (&isFree(xc,yc)&&start){
if (turn==1){
drawBlack(g,xc,yc);
gp.setMap(xc,yc,1);
message="白棋";
turn=2; //实现黑白交替
}else if(turn==2){
drawWhite(g,xc,yc);
gp.setMap(xc,yc,2);
message="黑棋";
turn=1;
}
}
stckx.push(xc); //将棋子坐标入栈
stcky.push(yc);
}
3.3 输赢判断
根据落子位置,判断落子位置上下左右,左上右下,右上左下的棋盘情况,之后对应方向相加。
要注意判断循环的边界,否则容易出现数组越界情况。
以右上为例:
//右上相同棋子统计
public int count_RightUp(int[][]map,int i,int j){
int count = 0;
int x=i;
int y=j;
while (x>=0&&y>0&&x<SQUARE_NUM&&y<=SQUARE_NUM){
if (map[x][y]!=0){
if (map[x][y]==map[x+1][y-1]){
count++;
x++;y--;
}else
return count;
}
else
break;
}
return count;
}
注意要处理已有棋子时的情况 用break跳出循环,否则会在获胜判断后,再次点击则进入死循环。
对落子位置的四个方向求和,判断输赢。
//全向统计
public void sumAll(int[][]map,int x,int y){
int sum1 = count_LeftUp(map,x,y)+count_RightDown(map,x,y);
int sum2 = count_LeftDown(map,x,y)+count_RightUp(map,x,y);
int sum3 = count_Up(map,x,y)+count_Down(map,x,y);
int sum4 = count_Left(map,x,y)+count_Right(map,x,y);
if(map[x][y]==1&&((sum1>=4)||(sum2>=4)||(sum3>=4)||(sum4>=4))){
flag=1;
}else if(map[x][y]==2&&((sum1>=4)||(sum2>=4)||(sum3>=4)||(sum4>=4))){
flag=2;
}else
flag=0;
}
//判断获胜方
public void isWinner(int flag){
if (flag == 1){
JOptionPane.showMessageDialog(null,"黑棋胜利");
this.start=false;
}else if(flag==2){
JOptionPane.showMessageDialog(null,"白棋胜利");
this.start=false;
}else{
System.out.println("暂无获胜者");
}
}
3.4 悔棋功能
悔棋方法定义在GamePanel类重写的actionPerformed方法下,从栈中获得最后一颗棋子坐标,取消该处棋子,之后重绘棋盘。
if (command.equals("back")) { //悔棋
if (!gm.stckx.isEmpty()) {
System.out.println("悔棋,返回上一步状态");
Integer temp_x = gm.stckx.pop();
Integer temp_y = gm.stcky.pop();
if (map[temp_x][temp_y] == 1) {
setMap(temp_x,temp_y,0);
gm.message = "黑棋";
gm.turn = 1;
repaint();
} else if (map[temp_x][temp_y] == 2) {
gm.message = "白棋";
gm.turn = 2;
setMap(temp_x,temp_y,0);
repaint();
} else if (gm.stckx.size() == 1) { //悔第一颗黑子
gm.message = " ";
gm.turn = 1;
setMap(temp_x,temp_y,0);
repaint();
}
}else
System.out.println("无法悔棋");
}
4.简单人机对战功能
使用权值法决定落子位置:在落子前对所有空位遍历,由周围棋子情况计算该处权值,之后在权值最大处落子。
首先利用哈希表保存棋子情况和设置的权重:此处只考虑人机下白棋情况
public class ChessAI implements ChessConfig{
public GamePanel gp;
public GameMouse gm;
public static HashMap<String,Integer> Weight = new HashMap<>();
int[][] value = new int[11][11];
public ChessAI() {
Weight.put("1", 20);
Weight.put("11",200);
Weight.put("111",800);
Weight.put("1111",2000);
Weight.put("2",20);
Weight.put("22",300);
Weight.put("222",900);
Weight.put("2222",4000);
}
遍历棋盘,对空位置分析其所有方向的权重,之后相加。
public int[] ai(int [][]map){
for (int i =0;i<=SQUARE_NUM;i++){
for (int j = 0;j<=SQUARE_NUM;j++){
if (map[i][j]==0){
//向右
String zeroPoint ="";
for(int temp=1;i+temp<=SQUARE_NUM;temp++){
int first = map[i+1][j];
if (map[i+temp][j]!=0&&map[i+temp][j]==first){
zeroPoint+=map[i+temp][j]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//向左
zeroPoint ="";
for(int temp=1;i-temp>=0;temp++){
int first = map[i-1][j];
if (map[i-temp][j]!=0&&map[i-temp][j]==first){
zeroPoint+=map[i-temp][j]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//向上
zeroPoint ="";
for(int temp=1;j-temp>=0;temp++){
int first = map[i][j-1];
if (map[i][j-temp]!=0&&map[i][j-temp]==first){
zeroPoint+=map[i][j-temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//向下
zeroPoint ="";
for(int temp=1;j+temp<=SQUARE_NUM;temp++){
int first = map[i][j+1];
if (map[i][j+temp]!=0&&map[i][j+temp]==first){
zeroPoint+=map[i][j+temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//向左上
zeroPoint ="";
for(int temp=1;i-temp>=0&&j-temp>=0;temp++){
int first = map[i-1][j-1];
if (map[i-temp][j-temp]!=0&&map[i-temp][j-temp]==first){
zeroPoint+=map[i-temp][j-temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//右下
zeroPoint ="";
for(int temp=1;i+temp<=SQUARE_NUM&&j+temp<=SQUARE_NUM;temp++){
int first = map[i+1][j+1];
if (map[i+temp][j+temp]!=0&&map[i+temp][j+temp]==first){
zeroPoint+=map[i+temp][j+temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//右上
zeroPoint ="";
for(int temp=1;i+temp<=SQUARE_NUM&&j-temp>=0;temp++){
int first = map[i+1][j-1];
if (map[i+temp][j-temp]!=0&&map[i+temp][j-temp]==first){
zeroPoint+=map[i+temp][j-temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
//左下
zeroPoint ="";
for(int temp=1;i-temp>=0&&j+temp<=SQUARE_NUM;temp++){
int first = map[i-1][j+1];
if (map[i-temp][j+temp]!=0&&map[i-temp][j+temp]==first){
zeroPoint+=map[i-temp][j+temp]+"";
}else
break; //碰到空位置就结束
}if (Weight.get(zeroPoint)!=null){
value[i][j]+=Weight.get(zeroPoint);
}
}
else
value[i][j]=0;
}
}
int max = 0;
int xx=0,yy=0;
for (int i=0;i<=SQUARE_NUM;i++){
for (int j=0;j<=SQUARE_NUM;j++){
if (max<value[i][j]){
max = value[i][j];
xx=i;
yy=j;
}
}
}
clearValue(value);
return new int[]{xx, yy};
}
对每个位置考虑周围相/不同子相连情况,这种算法较为简单,没有将棋子两个对应方向(如左方和右方)的情况共同考虑。容易出现下图情况:
4.1 对落子功能的修改
加入人机对战功能后,将人机落子放在玩家落子后,只考虑人机执白子情况:
if (isFree(xc,yc)&&start){
if (turn==1){
drawBlack(g,xc,yc);
gp.setMap(xc,yc,1);
message="白棋";
turn=2;
sumAll(gp.getMap(),xc,yc);
if (AI&&flag!=1){
xc=ai.ai(gp.getMap())[0];
yc=ai.ai(gp.getMap())[1];
drawWhite(g,xc,yc);
gp.setMap(xc,yc,2);
message="黑棋";
turn=1;
}
}else if(turn==2&&!AI){
drawWhite(g,xc,yc);
gp.setMap(xc,yc,2);
message="黑棋";
turn=1;
}
}
此时下棋黑棋后需要单独使用进行判断输赢的sumAll()方法,否则会出现黑棋五子却没有提示胜利,没有结束游戏的情况。
5.小结
作为第一个java项目,本项目改进之处还有很多,例如算法和人机执黑子功能。
完整项目代码:https://github.com/smdnzhan/FiveChess