【Java - 石头迷阵游戏】基于JavaSE面向对象

发布时间 2023-11-29 17:13:30作者: 沙汀鱼

石头迷阵游戏

  • 初始界面

  • 胜利界面

游戏说明

  1. 可以用上下左右按键控制石头移动,直到石块按照顺序排列游戏成功。
  2. 显示移动步数
  3. 可以重新游戏

技术说明

  1. GUI设计:JFrame窗体、JLable组件(文本、按钮、图片)
  2. 类的继承(继承JFrame类)
  3. 接口的实现(实现KeyListener接口)
  4. 匿名内部类和Lambda表达式实现多态
  5. 事件监听(窗体.键盘按键监听+按钮.鼠标按键监听)
  6. 异或操作实现变量数值交换【没想到这个是调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();
    }
}