【Java - 石头迷阵游戏】基于JavaSE面向对象
石头迷阵游戏
-
初始界面
-
胜利界面
游戏说明
- 可以用上下左右按键控制石头移动,直到石块按照顺序排列游戏成功。
- 显示移动步数
- 可以重新游戏
技术说明
- GUI设计:JFrame窗体、JLable组件(文本、按钮、图片)
- 类的继承(继承JFrame类)
- 接口的实现(实现KeyListener接口)
- 匿名内部类和Lambda表达式实现多态
- 事件监听(窗体.键盘按键监听+按钮.鼠标按键监听)
- 异或操作实现变量数值交换【没想到这个是调bug最久的地方,由于异或了同一个变量导致变量数值变为0而出错】
代码
MainFrame.java
package com.EveX.mazeofstones;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
/**
* 创建类MainFrame
* 继承JFrame类:便于后续增加窗体方法【由于MainFrame继承了JFrame类,因此可以直接调用父类的成员方法(可以省略this/super关键字)】
* 实现KeyListener接口:便于监听键盘操作
*/
public class MainFrame extends JFrame implements KeyListener{
/**
* 石块图片的位置数据
*/
private int[][] data = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11},
{12, 13, 14, 15},
};
/**
* 0号石块位置索引
*/
private int x0 = 0;
private int y0 = 0;
/**
* 胜利数组
*/
private final int[][] WIN_DATA = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0},
};
/**
* 统计有效步数
*/
private int step = 0;
/**
* 构造函数
* 创建对象则完成窗体初始化和绘制操作
*/
public MainFrame() {
// 添加键盘监听【这里因为MainFrame实现了KeyListener接口,因此实现类对象就是自己的对象】
addKeyListener(this);
print();
// 打乱石头方块
shuffleData();
// 初始化窗体界面
initFrame();
// 绘制游戏的窗体界面
paintView();
// 设置窗体可见,放在最后
setVisible(true);
}
/**
* 打乱石头方块
* 本质上是打乱data数组中数值的索引位置,从而来实现打乱石头方块的目的
*/
public void shuffleData() {
Random r = new Random();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
int x = r.nextInt(4); // 产生[0, 4)范围内的随机数
int y = r.nextInt(4);
swap(i, j, x, y);
print();
}
}
// 获取0号石块的数组索引
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if(data[i][j] == 0) {
x0 = i;
y0 = j;
break;
}
}
}
}
/**
* 打印data数组(用于调试的)
*/
private void print() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
System.out.print(data[i][j] + " ");
}
System.out.println();
}
}
/**
* 交换二维数组data中的data[i][j]和data[x][y]对应数值;
*/
private void swap(int i, int j, int x, int y) {
// 如果是同一个变量则不进行异或交换,不然会出现该变量数值变为0的bug!
if(i == x && j == y) return;
System.out.println("交换前:data" + "[" + i + "]" + "[" + j + "]:" + data[i][j] + "和data" + "[" + x + "]" + "[" + y + "]:" + data[x][y]);
data[i][j] = data[i][j] ^ data[x][y];
data[x][y] = data[i][j] ^ data[x][y];
data[i][j] = data[i][j] ^ data[x][y];
System.out.println("交换后:data" + "[" + i + "]" + "[" + j + "]:" + data[i][j] + "和data" + "[" + x + "]" + "[" + y + "]:" + data[x][y]);
}
/**
* 初始化窗体界面
*/
public void initFrame() {
// 设置窗体大小
setSize(514, 595);
// 设置窗体关闭模式——窗体关闭则程序停止运行
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 设置窗体置顶
setAlwaysOnTop(true);
// 设置窗体居中,必须放在设置窗体大小之后
setLocationRelativeTo(null);
// 设置窗体名字
setTitle("石头迷阵单机版V1.0");
// 取消组件的默认布局
setLayout(null);
}
/**
* 绘制游戏的窗体界面 -> 加载背景图片
*/
public void paintView() {
// 清空所有组件
getContentPane().removeAll();
// 如果游戏胜利,展示胜利的图片
if(isWin()) showWin();
// 添加计数器显示
JLabel txt = new JLabel("步数:" + step);
txt.setBounds(50, 20, 100, 20);
getContentPane().add(txt);
// 添加重新游戏按钮
JButton btn = new JButton("重新游戏");
btn.setBounds(350, 20, 100, 20);
getContentPane().add(btn);
btn.setFocusable(false); // 取消按钮组件的焦点
// 添加鼠标动作事件
btn.addActionListener(e -> {
// 计数器归0
step = 0;
// 打乱石头方块
shuffleData();
// 绘制游戏的窗体界面
paintView();
});
// 按照data数组添加石头方块的图片
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
JLabel pic = new JLabel(new ImageIcon("/Users/evex/Projects/IDEA/Advanced-Codes/day04-code/image/" + data[i][j] + ".png"));
int y = 90 + i * 100;
int x = 50 + j * 100;
pic.setBounds(x, y, 100, 100);
getContentPane().add(pic);
}
}
// 添加背景图片,Label添加是遵循栈原理,先进先出,因此需要最后放背景图,才能显示在石头方块的后面
JLabel background = new JLabel(new ImageIcon("/Users/evex/Projects/IDEA/Advanced-Codes/day04-code/image/background.png"));
background.setBounds(26, 30, 450, 484);
getContentPane().add(background);
// 刷新窗体界面
getContentPane().repaint();
}
// 展示胜利的图片
private void showWin() {
JLabel pic = new JLabel(new ImageIcon("/Users/evex/Projects/IDEA/Advanced-Codes/day04-code/image/win.png"));
int x = 124;
int y = 230;
pic.setBounds(x, y, 266, 88);
getContentPane().add(pic);
}
/**
* 键盘方向键按下触发移动
*/
private void move(int keyCode) {
// 如果游戏胜利,则禁止再次进行移动
if(isWin()) return;
if(keyCode == 37) { // 键盘左键
// System.out.println("键盘左键");
int tarX = x0;
int tarY = y0 + 1;
moveOne(tarX, tarY);
} else if(keyCode == 38) { // 键盘上键
// System.out.println("键盘上键");
int tarX = x0 + 1;
int tarY = y0;
moveOne(tarX, tarY);
} else if(keyCode == 39) { // 键盘右键
// System.out.println("键盘右键");
int tarX = x0;
int tarY = y0 - 1;
moveOne(tarX, tarY);
} else if(keyCode == 40) { // 键盘下键
// System.out.println("键盘下键");
int tarX = x0 - 1;
int tarY = y0;
moveOne(tarX, tarY);
} else if(keyCode == 87) { // w键
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
data[i][j] = WIN_DATA[i][j];
}
}
}
}
/**
* 根据上下左右按键交换0号石块和旁边石块
*/
private void moveOne(int tarX, int tarY) {
if(!in(tarX, tarY)) {
System.out.println("非法操作");
return;
}
swap(x0, y0, tarX, tarY);
// 更新0号石块的位置
x0 = tarX;
y0 = tarY;
// 步数+1
++ step;
print();
}
/**
* 判断索引(x, y)是否在data范围内
*/
private boolean in(int x, int y) {
return x >= 0 && x < 4 && y >= 0 && y < 4;
}
/**
* 判断当前游戏是否胜利
*/
private boolean isWin() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if(data[i][j] != WIN_DATA[i][j]) return false;
}
}
return true;
}
//---------------------------------
/**
* 继承KeyListener接口需要重写下述三个抽象方法
* 实际上只用到了KeyPressed() -> 键盘按键按下监听
*/
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// System.out.println(keyCode);
move(keyCode);
paintView();
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
//---------------------------------
}
Test.java
package com.EveX.mazeofstones;
public class Test {
public static void main(String[] args) {
new MainFrame();
}
}