Java基础篇09(2):项目实战之基于swing的石头迷阵

1 本节目标

  • 使用 Java Swing 开发一个 4×4 的数字华容道游戏(石头迷阵)。
  • 游戏规则:点击键盘上下左右键,移动数字方块,将数字按 1~15 顺序排列,0(空白)位于右下角。
  • 实现功能:随机打乱、步数统计、胜利检测、胜利图片显示、系统菜单(重启/退出)。
  • 最终效果:你读完这篇博客后,能够独立写出完整的游戏代码。

2 整体开发思路(按顺序走)

3关键代码(分模块实现)

3.1 窗口框架(initFrame)

java 复制代码
private void initFrame() {
    setTitle("石头迷阵 v1.0");
    setSize(465, 575);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setLayout(null);   // 绝对定位,便于控制每个图片的精确位置
}

为什么绝对布局 :拼图游戏需要手动计算每个方块的 (x, y) 坐标(公式:20 + j100, 60 + i100),null 布局配合 setBounds 最直观。

3.2 数据结构与胜利状态

java 复制代码
// 当前游戏数据(0 代表空白块)
private int[][] imageData = {
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12},
    {13,14,15,0}
};

// 胜利时的目标状态
private int[][] winData = {
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12},
    {13,14,15,0}
};

private int row = 3, col = 3;   // 空白块当前位置(初始在右下角)
private int count = 0;           // 移动步数

方向枚举

java 复制代码
enum Direction { UP, DOWN, LEFT, RIGHT }

3.3 绘制界面(initImage)

每次移动后都需要重新绘制,所以将清空窗口、添加所有组件(步数标签、数字图片、胜利图片、背景)集中在此方法中。

java 复制代码
private void initImage() {
    // 1. 清除所有已有组件
    getContentPane().removeAll();

    // 2. 显示步数
    JLabel stepLabel = new JLabel("移动步数:" + count);
    stepLabel.setBounds(20, 20, 120, 30);
    stepLabel.setFont(new Font("楷体", Font.BOLD, 14));
    stepLabel.setForeground(Color.RED);
    add(stepLabel);

    // 3. 绘制数字方块(根据 imageData)
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            int num = imageData[i][j];
            // 图片路径,例如 image/1.png、image/0.png(空白图)
            ImageIcon icon = new ImageIcon("image/" + num + ".png");
            JLabel label = new JLabel(icon);
            label.setBounds(20 + j * 100, 60 + i * 100, 100, 100);
            add(label);
        }
    }

    // 4. 胜利时额外显示胜利图片(覆盖在窗口中央)
    if (isWin()) {
        JLabel winLabel = new JLabel(new ImageIcon("image/win.png"));
        winLabel.setBounds(124, 230, 266, 88);
        add(winLabel);
    }

    // 5. 背景图片(必须最后添加,避免遮挡其他组件?实际背景应最先添加,但最后添加会被覆盖?)
    // 经验证:Swing 中后添加的组件会显示在上层,所以背景要最先添加。这里调整顺序。
    // 正确做法:先添加背景,再添加其他组件。但为了代码清晰,此处说明。
    JLabel bg = new JLabel(new ImageIcon("image/background.png"));
    bg.setBounds(0, 0, 450, 484);
    add(bg);

    // 6. 刷新窗口
    repaint();
}

常见坑:背景图片如果最后添加,会覆盖掉数字图片。解决方案:将背景图片添加的代码移到最前面(在 removeAll() 之后立即添加背景,然后再添加步数和数字图片)。上面代码顺序仅为说明,实际使用时请自行调整。

3.4 打乱算法(保证有解)

最常见的错误是随机交换元素可能导致无解。正确做法:从胜利状态出发,随机执行多次合法移动

java 复制代码
private void shuffle(int steps) {
    Random rand = new Random();
    for (int s = 0; s < steps; s++) {
        int dir = rand.nextInt(4);  // 0上 1下 2左 3右
        switch (dir) {
            case 0: // 上 → 空白块与下方块交换(空白向下移动)
                if (row < 3) {
                    swap(row, col, row+1, col);
                    row++;
                }
                break;
            case 1: // 下
                if (row > 0) {
                    swap(row, col, row-1, col);
                    row--;
                }
                break;
            case 2: // 左
                if (col < 3) {
                    swap(row, col, row, col+1);
                    col++;
                }
                break;
            case 3: // 右
                if (col > 0) {
                    swap(row, col, row, col-1);
                    col--;
                }
                break;
        }
    }
}

// 交换二维数组中的两个元素
private void swap(int r1, int c1, int r2, int c2) {
    int temp = imageData[r1][c1];
    imageData[r1][c1] = imageData[r2][c2];
    imageData[r2][c2] = temp;
}

3.5 键盘监听与移动逻辑

使用 KeyAdapter 监听窗口的按键事件,根据方向调用 tryMove 方法。

java 复制代码
private void initKeyPressEvent() {
    this.addKeyListener(new KeyAdapter() {
        @Override
        public void keyPressed(KeyEvent e) {
            int key = e.getKeyCode();
            switch (key) {
                case KeyEvent.VK_UP:    tryMove(Direction.UP); break;
                case KeyEvent.VK_DOWN:  tryMove(Direction.DOWN); break;
                case KeyEvent.VK_LEFT:  tryMove(Direction.LEFT); break;
                case KeyEvent.VK_RIGHT: tryMove(Direction.RIGHT); break;
            }
        }
    });
}

// 移动逻辑(与打乱中的方向定义一致)
private void tryMove(Direction dir) {
    switch (dir) {
        case UP:
            if (row < 3) {
                swap(row, col, row+1, col);
                row++;
                count++;
            }
            break;
        case DOWN:
            if (row > 0) {
                swap(row, col, row-1, col);
                row--;
                count++;
            }
            break;
        case LEFT:
            if (col < 3) {
                swap(row, col, row, col+1);
                col++;
                count++;
            }
            break;
        case RIGHT:
            if (col > 0) {
                swap(row, col, row, col-1);
                col--;
                count++;
            }
            break;
    }
    initImage();   // 移动后刷新界面
}

3.6 胜利检测

java 复制代码
private boolean isWin() {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (imageData[i][j] != winData[i][j]) {
                return false;
            }
        }
    }
    return true;
}

3.7 菜单栏(退出/重启)

java 复制代码
private void initMenu() {
    JMenuBar menuBar = new JMenuBar();
    JMenu menu = new JMenu("系统");
    JMenuItem restartItem = new JMenuItem("重启");
    JMenuItem exitItem = new JMenuItem("退出");
    menu.add(restartItem);
    menu.add(exitItem);
    menuBar.add(menu);
    setJMenuBar(menuBar);

    // 重启游戏
    restartItem.addActionListener(e -> {
        // 重置为胜利状态
        initBoardToWin();
        // 重新打乱
        shuffle(200);
        count = 0;
        initImage();
    });

    // 退出游戏
    exitItem.addActionListener(e -> System.exit(0));
}

// 重置数组为胜利顺序(并重新定位空白位置)
private void initBoardToWin() {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            imageData[i][j] = winData[i][j];
        }
    }
    row = 3;
    col = 3;
}

3.8 构造方法中调用所有初始化

java 复制代码
public MainFrame() {
    initFrame();          // 窗口基础设置
    initBoardToWin();     // 数组置为胜利顺序(可选)
    shuffle(200);         // 打乱
    initImage();          // 绘制界面
    initKeyPressEvent();  // 键盘监听
    initMenu();           // 菜单栏
    setVisible(true);     // 显示窗口
}

4 成果展示

相关推荐
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 08 | Agent 的自我进化:nudge、后台审查与轨迹数据
java·前端·人工智能
宸津-代码粉碎机1 小时前
Spring AI 企业级RAG实战|增量更新+文档去重+定时自动入库生产落地方案
java·大数据·人工智能·后端·python·spring
Raink老师1 小时前
【AI面试临阵磨枪-92】Skill 开发规范:命名、文档、测试、日志、监控、告警?
java·面试·log4j
Evand J1 小时前
【代码介绍】自适应R的AEKF(自适应扩展卡尔曼滤波)和经典EKF比较,MATLAB例程|三维非线性系统
开发语言·matlab·ekf·自适应·自适应滤波
雪的季节2 小时前
1 个网络线程 + 3 个数据处理线程(完全隔离)
开发语言
weixin_408099672 小时前
2026 AI生成图片快速去水印的5种实测方法(附在线工具 + Python/Java/PHP API代码)
java·人工智能·python·api接口·ai去水印·石榴智能·自动去水印
风筝在晴天搁浅2 小时前
快手 CodeTop LeetCode 227.基本计算器Ⅱ
java·开发语言
JAVA面经实录9172 小时前
RabbitMQ全套学习知识手册
java·rabbitmq