news 2026/2/19 2:04:18

基于Java Swing的连连看小游戏(2)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Java Swing的连连看小游戏(2)

1、演示视频

基于Java Swing的连连看小游戏

2、项目截图

设计说明

3.1 整体架构设计

本项目采用单一主类LinkGame继承JFrame作为程序入口,内部包含多个私有方法和内部类,遵循“单一职责原则”将功能模块拆分:

  • 界面初始化模块initUI()方法负责创建窗口、菜单栏、按钮、信息面板、游戏面板等界面元素,并绑定事件监听器。
  • 游戏数据管理模块:包含游戏地图(二维数组)、步数、时间、选中状态等数据,以及initGame()updateInfoLabels()等方法管理数据。
  • 游戏逻辑处理模块:包含handleSelect()(选中处理)、isConnectable()(连接判断)、isDeadlock()(死局检测)等核心逻辑方法。
  • 界面绘制模块:内部类GamePanel继承JPanel,重写paintComponent()方法实现游戏元素的绘制。
  • 工具方法模块:包含getCenterX()getCenterY()等工具方法,提供通用的坐标计算功能。

3.2 数据结构设计

  • 游戏地图:使用二维数组int[][] gameMap存储每个格子的图片编号,0表示空格子,非0值表示对应编号的图片。
  • 选中状态:使用Point类型的变量firstSelectsecondSelect存储玩家选中的两个格子的坐标。
  • 图片资源:使用List存储加载的图片资源,索引与图片编号对应。
  • 计时与步数:使用int类型变量currentSteps(当前步数)、remainingTime(剩余时间)存储游戏进度数据。

3.3 界面布局设计

界面采用BorderLayout布局管理器,分为三个部分:

  1. 顶部面板(NORTH):包含步数、时间信息标签,以及“重新开始”“洗牌”按钮,使用FlowLayout布局保证元素排列整齐。
  2. 中间面板(CENTER):游戏面板,负责绘制格子、图片、选中效果和连线。
  3. 菜单栏:包含“游戏”菜单,提供“重新开始”“洗牌”“退出”选项。

四、算法说明

4.1 图片连接判断算法

核心算法是判断两个图片是否可连接,支持三种连接方式:直连、单拐点、双拐点,算法流程如下:

4.1.1 直连判断(isDirectConnect方法)

判断两个点是否在同一行或同一列,且中间的格子均为空(图片编号为0)。

  • 同行:遍历两个点之间的所有x坐标,检查对应的格子是否为空。
  • 同列:遍历两个点之间的所有y坐标,检查对应的格子是否为空。
4.1.2 单拐点判断(findSingleCorner方法)

寻找两个点的拐点(候选拐点为(p1.x, p2.y)(p2.x, p1.y)),判断拐点是否为空,且拐点与两个点分别直连。

4.1.3 双拐点判断(findDoubleCorner方法)

遍历所有空格子作为第一个拐点,判断该拐点与第一个点直连,且存在第二个拐点与第一个拐点和第二个点分别直连(复用单拐点判断逻辑)。

4.2 死局检测算法(isDeadlock方法)

遍历所有非空格子的坐标,两两配对检测是否存在编号相同且可连接的图片对:

  1. 收集所有非空格子的坐标到列表posList中。
  2. 双重循环遍历列表,对每一对坐标判断图片编号是否相同且可连接。
  3. 若存在这样的图片对,返回false(非死局);否则返回true(死局)。

4.3 洗牌算法(shuffleGameMap方法)

打乱现有非空图片的布局,保留空格,保证游戏连续性:

  1. 收集所有非空格子的图片编号和对应坐标到两个列表中。
  2. 使用Collections.shuffle()方法打乱图片编号列表。
  3. 将打乱后的图片编号放回原非空格子的坐标位置。
  4. 洗牌后再次检测死局,若仍为死局则提示玩家重新开始。

五、测试说明

5.1 测试环境

测试项配置信息
操作系统Windows 10/11、macOS、Linux(Ubuntu)
Java版本JDK 8、JDK 11、JDK 17(兼容)
开发工具IntelliJ IDEA、Eclipse、VS Code
屏幕分辨率1920×1080及以上(推荐)

5.2 功能测试用例

测试用例编号测试功能测试步骤预期结果
TC001图片消除(直连)1. 启动游戏;2. 点击同一行的两个相同图片,中间无遮挡两个图片被消除,步数加1
TC002图片消除(单拐点)1. 启动游戏;2. 点击两个相同图片,存在单拐点连接两个图片被消除,步数加1,显示拐点折线
TC003胜利判定1. 启动游戏;2. 消除所有图片弹出胜利提示框,显示步数和剩余时间
TC004失败判定(步数用尽)1. 启动游戏;2. 选择超过最大步数的图片对弹出失败提示框,提示步数用尽
TC005失败判定(时间耗尽)1. 启动游戏;2. 等待时间耗尽弹出失败提示框,提示时间耗尽
TC006死局检测与洗牌1. 启动游戏;2. 消除图片至死局状态弹出死局提示框,选择洗牌后图片布局打乱,选择放弃则游戏失败
TC007手动洗牌1. 启动游戏;2. 点击“洗牌”按钮非空图片布局打乱,空格保留
TC008重新开始1. 启动游戏;2. 点击“重新开始”按钮游戏地图重置,步数和时间恢复初始值
TC009图片资源适配1. 不放置本地图片;2. 启动游戏游戏正常运行,使用随机颜色块替代图片

5.3 边界测试

  • 行列数边界:测试6×6、8×8、10×10(需保证乘积为偶数)的地图大小,验证游戏功能是否正常。
  • 步数边界:测试最大步数设置为10、50、100时,步数用尽的失败判定是否正常。
  • 时间边界:测试最大时间设置为10、60、120秒时,时间耗尽的失败判定是否正常。
  • 死局边界:测试仅剩两个不可连接的相同图片时,死局检测是否准确。

5.4 兼容性测试

  • 在不同操作系统(Windows、macOS、Linux)下测试游戏界面显示和功能是否正常。
  • 在不同JDK版本(8、11、17)下测试代码编译和运行是否正常。
  • 在不同屏幕分辨率下测试游戏界面布局是否错乱。

六、关键代码

6.1 核心:图片连接判断代码

/** * 判断两个点是否可连接(直连、单拐点、双拐点) * @param p1 第一个点 * @param p2 第二个点 * @return 是否可连接 */ private boolean isConnectable(Point p1, Point p2) { // 直连判断(同行或同列,中间无遮挡) if (isDirectConnect(p1, p2)) { return true; } // 单拐点判断(存在一个中间点,分别与两个点直连) Point corner = findSingleCorner(p1, p2); if (corner != null) { return true; } // 双拐点判断(存在两个中间点,形成路径) return findDoubleCorner(p1, p2); } /** * 直连判断(同行或同列,中间无遮挡) */ private boolean isDirectConnect(Point p1, Point p2) { int x1 = p1.x, y1 = p1.y; int x2 = p2.x, y2 = p2.y; // 同行 if (y1 == y2) { int minX = Math.min(x1, x2); int maxX = Math.max(x1, x2); for (int x = minX + 1; x < maxX; x++) { if (gameMap[y1][x] != 0) { return false; } } return true; } // 同列 if (x1 == x2) { int minY = Math.min(y1, y2); int maxY = Math.max(y1, y2); for (int y = minY + 1; y < maxY; y++) { if (gameMap[y][x1] != 0) { return false; } } return true; } return false; } /** * 查找单拐点(存在一个点,分别与p1、p2直连),并返回拐点(用于绘制折线) */ private Point findSingleCorner(Point p1, Point p2) { int x1 = p1.x, y1 = p1.y; int x2 = p2.x, y2 = p2.y; // 拐点在(p1.x, p2.y) Point corner1 = new Point(x1, y2); if ((gameMap[corner1.y][corner1.x] == 0 || corner1.equals(p1) || corner1.equals(p2)) && isDirectConnect(p1, corner1) && isDirectConnect(corner1, p2)) { return corner1; } // 拐点在(p2.x, p1.y) Point corner2 = new Point(x2, y1); if ((gameMap[corner2.y][corner2.x] == 0 || corner2.equals(p1) || corner2.equals(p2)) && isDirectConnect(p1, corner2) && isDirectConnect(corner2, p2)) { return corner2; } return null; } /** * 双拐点判断(遍历所有空点,判断是否存在两个拐点形成路径) */ private boolean findDoubleCorner(Point p1, Point p2) { // 遍历所有空位置作为第一个拐点 for (int y = 0; y < ROW; y++) { for (int x = 0; x < COL; x++) { if (gameMap[y][x] != 0 && !(x == p1.x && y == p1.y) && !(x == p2.x && y == p2.y)) { continue; } Point corner1 = new Point(x, y); // 第一个拐点与p1直连,且存在第二个拐点与corner1、p2分别直连 if (isDirectConnect(p1, corner1)) { Point corner2 = findSingleCorner(corner1, p2); if (corner2 != null) { return true; } } } } return false; }

6.2 死局检测与洗牌代码

/** * 死局检测:判断是否存在至少一对可连接的图片 * @return true=死局(无可用对),false=有可用对 */ private boolean isDeadlock() { // 遍历所有图片位置,两两配对检测 List posList = new ArrayList<>(); for (int y = 0; y < ROW; y++) { for (int x = 0; x < COL; x++) { if (gameMap[y][x] != 0) { posList.add(new Point(x, y)); } } } // 遍历所有两两组合 for (int i = 0; i < posList.size(); i++) { Point p1 = posList.get(i); for (int j = i + 1; j < posList.size(); j++) { Point p2 = posList.get(j); // 图片编号相同且可连接 if (gameMap[p1.y][p1.x] == gameMap[p2.y][p2.x] && isConnectable(p1, p2)) { return false; // 存在可用对,不是死局 } } } return true; // 无可用对,死局 } /** * 洗牌:打乱现有非空图片的布局,保留空格 */ private void shuffleGameMap() { // 1. 收集所有非空位置的图片编号 List imgList = new ArrayList<>(); List posList = new ArrayList<>(); for (int y = 0; y < ROW; y++) { for (int x = 0; x < COL; x++) { if (gameMap[y][x] != 0) { imgList.add(gameMap[y][x]); posList.add(new Point(x, y)); } } } // 2. 打乱图片编号顺序 Collections.shuffle(imgList); // 3. 将打乱后的图片编号放回原非空位置 for (int i = 0; i < posList.size(); i++) { Point p = posList.get(i); gameMap[p.y][p.x] = imgList.get(i); } // 重置选中状态 firstSelect = null; secondSelect = null; isSecondSelectInvalid = false; // 重绘面板 repaint(); // 洗牌后再次检测,如果还是死局,提示重新开始 if (isDeadlock()) { int result = JOptionPane.showConfirmDialog(this, "洗牌后仍无可用消除的图片,是否重新开始游戏?", "提示", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { restartGame(); } } }

6.3 选中处理与胜负判定代码

/** * 处理选中逻辑(新增步数统计、失败判断、死局检测) * @param point 选中的格子坐标(x:列,y:行) */ private void handleSelect(Point point) { isSecondSelectInvalid = false; if (firstSelect == null) { // 第一次选中 firstSelect = point; } else if (firstSelect.equals(point)) { // 点击同一个位置,取消选中 firstSelect = null; secondSelect = null; } else { // 第二次选中,步数+1 currentSteps++; updateInfoLabels(); secondSelect = point; // 判断步数是否超过最大值,游戏失败 if (currentSteps > MAX_STEPS) { timer.stop(); showGameResult(false); return; } // 判断是否是相同的图片 if (gameMap[firstSelect.y][firstSelect.x] == gameMap[secondSelect.y][secondSelect.x]) { // 判断是否可连接 if (isConnectable(firstSelect, secondSelect)) { // 消除图片(置为0) gameMap[firstSelect.y][firstSelect.x] = 0; gameMap[secondSelect.y][secondSelect.x] = 0; // 消除后检测是否胜利 if (isGameWin()) { timer.stop(); showGameResult(true); } else { // 消除后检测是否死局 if (isDeadlock()) { int result = JOptionPane.showConfirmDialog(this, "当前无可用消除的图片,是否进行洗牌?", "死局提示", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { shuffleGameMap(); // 洗牌 } else { // 不洗牌则游戏失败 timer.stop(); JOptionPane.showMessageDialog(this, "游戏失败:无可用消除的图片", "失败", JOptionPane.INFORMATION_MESSAGE); restartGame(); } } } // 重置选中状态 firstSelect = null; secondSelect = null; } else { // 不可连接,标记为无效选中,用于闪烁提示 isSecondSelectInvalid = true; } } else { // 图片不同,标记为无效选中 isSecondSelectInvalid = true; } } } /** * 游戏胜利判断 */ private boolean isGameWin() { for (int y = 0; y < ROW; y++) { for (int x = 0; x < COL; x++) { if (gameMap[y][x] != 0) { return false; } } } return true; } /** * 显示游戏结果(胜利/失败) * @param isWin 是否胜利 */ private void showGameResult(boolean isWin) { String title = isWin ? "胜利" : "失败"; String message = isWin ? "恭喜你,游戏胜利!\n步数:" + currentSteps + "\n剩余时间:" + remainingTime + "秒" : "很遗憾,游戏失败!\n步数已用尽/时间已到"; int result = JOptionPane.showConfirmDialog(this, message + "\n是否重新开始?", title, JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { restartGame(); } else { System.exit(0); } }

6.4 界面绘制与折线连线代码

/** * 游戏面板,负责绘制游戏元素(优化选中样式、连线、悬浮效果) */ private class GamePanel extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿 // 绘制游戏格子和图片 for (int y = 0; y < ROW; y++) { for (int x = 0; x < COL; x++) { // 计算格子的坐标 int posX = GAP + x * (CELL_SIZE + GAP); int posY = GAP + y * (CELL_SIZE + GAP); // 绘制格子背景 g2d.setColor(Color.LIGHT_GRAY); g2d.drawRect(posX, posY, CELL_SIZE, CELL_SIZE); // 绘制图片(编号不为0时) int imgNum = gameMap[y][x]; if (imgNum > 0) { BufferedImage img = images.get(imgNum - 1); // 消除时的渐变效果(简单模拟:降低透明度) float alpha = 1.0f; g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); g2d.drawImage(img, posX, posY, this); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); // 鼠标悬浮效果:浅灰色边框 if (hoverPoint != null && hoverPoint.x == x && hoverPoint.y == y) { g2d.setColor(Color.GRAY); g2d.setStroke(new BasicStroke(2)); g2d.drawRect(posX, posY, CELL_SIZE, CELL_SIZE); g2d.setStroke(new BasicStroke(1)); } // 绘制第一个选中的边框(红色粗边框) if (firstSelect != null && firstSelect.x == x && firstSelect.y == y) { g2d.setColor(Color.RED); g2d.setStroke(new BasicStroke(3)); g2d.drawRect(posX, posY, CELL_SIZE, CELL_SIZE); g2d.setStroke(new BasicStroke(1)); } // 绘制第二个选中的边框(无效则闪烁红色边框,有效则红色粗边框) if (secondSelect != null && secondSelect.x == x && secondSelect.y == y) { if (isSecondSelectInvalid) { // 闪烁效果:交替显示红色边框和无边框 if (System.currentTimeMillis() % 600 < 300) { g2d.setColor(Color.RED); g2d.setStroke(new BasicStroke(3)); g2d.drawRect(posX, posY, CELL_SIZE, CELL_SIZE); g2d.setStroke(new BasicStroke(1)); } } else { g2d.setColor(Color.RED); g2d.setStroke(new BasicStroke(3)); g2d.drawRect(posX, posY, CELL_SIZE, CELL_SIZE); g2d.setStroke(new BasicStroke(1)); } } } } } // 绘制连线(优化为折线,显示拐点) if (firstSelect != null && secondSelect != null && gameMap[firstSelect.y][firstSelect.x] == gameMap[secondSelect.y][secondSelect.x]) { if (isConnectable(firstSelect, secondSelect)) { drawLineWithCorner(g2d, firstSelect, secondSelect); } } } /** * 绘制带拐点的折线(直连/单拐点/双拐点) */ private void drawLineWithCorner(Graphics2D g2d, Point p1, Point p2) { // 调用类的工具方法获取中心坐标 int x1 = getCenterX(p1), y1 = getCenterY(p1); int x2 = getCenterX(p2), y2 = getCenterY(p2); g2d.setColor(Color.RED); g2d.setStroke(new BasicStroke(2)); // 直连:直接画直线 if (isDirectConnect(p1, p2)) { g2d.drawLine(x1, y1, x2, y2); } else { // 单拐点:画折线(p1 -> 拐点 -> p2) Point corner = findSingleCorner(p1, p2); if (corner != null) { int cx = getCenterX(corner); int cy = getCenterY(corner); g2d.drawLine(x1, y1, cx, cy); g2d.drawLine(cx, cy, x2, y2); } else { // 双拐点:简化处理,直接画直线(可扩展为查找双拐点后画折线) g2d.drawLine(x1, y1, x2, y2); } } g2d.setStroke(new BasicStroke(1)); } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/19 0:27:38

解锁数学智慧宝库:经典数学著作PDF免费获取指南 [特殊字符]

解锁数学智慧宝库&#xff1a;经典数学著作PDF免费获取指南 &#x1f3af; 【免费下载链接】数学它的内容方法和意义-全三卷.pdf下载仓库分享 本仓库提供了一个重要的数学资源文件的下载&#xff0c;文件名为《数学—它的内容、方法和意义-全三卷.pdf》。该文件详细介绍了数学的…

作者头像 李华
网站建设 2026/2/18 11:30:57

嵌入式开发新选择:10分钟掌握PlatformIO Core自动化构建技巧

嵌入式开发新选择&#xff1a;10分钟掌握PlatformIO Core自动化构建技巧 【免费下载链接】platformio-core Your Gateway to Embedded Software Development Excellence :alien: 项目地址: https://gitcode.com/gh_mirrors/pl/platformio-core PlatformIO Core是嵌入式软…

作者头像 李华
网站建设 2026/2/13 3:02:35

终极字体转换指南:轻松实现TTC与TTF互转

终极字体转换指南&#xff1a;轻松实现TTC与TTF互转 【免费下载链接】TTC与TTF字库文件转换教程及工具 ttctools是一款专为字体文件转换设计的开源工具&#xff0c;支持在TTC&#xff08;TrueType字体集合&#xff09;与TTF&#xff08;TrueType字体&#xff09;格式之间轻松转…

作者头像 李华
网站建设 2026/2/12 5:57:43

Java毕设项目:基于springboot二手图书交易系统基于SpringBoot+Vue的二手图书交易系统(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/2/4 3:11:54

Web Audio API 终极指南:从零开始掌握浏览器音频编程

Web Audio API 终极指南&#xff1a;从零开始掌握浏览器音频编程 【免费下载链接】web-audio-api The Web Audio API v1.0, developed by the W3C Audio WG 项目地址: https://gitcode.com/gh_mirrors/we/web-audio-api 想要在网页中实现专业级的音频效果吗&#xff1f;…

作者头像 李华
网站建设 2026/2/18 9:21:17

Java面试必问!线程与进程的核心区别你必须掌握!

文章目录Java面试必问&#xff01;线程与进程的核心区别你必须掌握&#xff01;一、什么是进程&#xff1f;进程的特点&#xff1a;示例代码&#xff1a;启动一个新的进程二、什么是线程&#xff1f;线程的特点&#xff1a;示例代码&#xff1a;启动两个线程三、进程与线程的核…

作者头像 李华