10条高精度ChatGPT提示词:面向知识工作的工程化设计
2026/6/7 10:24:45
基于Java Swing的连连看小游戏
本项目采用单一主类LinkGame继承JFrame作为程序入口,内部包含多个私有方法和内部类,遵循“单一职责原则”将功能模块拆分:
initUI()方法负责创建窗口、菜单栏、按钮、信息面板、游戏面板等界面元素,并绑定事件监听器。initGame()、updateInfoLabels()等方法管理数据。handleSelect()(选中处理)、isConnectable()(连接判断)、isDeadlock()(死局检测)等核心逻辑方法。GamePanel继承JPanel,重写paintComponent()方法实现游戏元素的绘制。getCenterX()、getCenterY()等工具方法,提供通用的坐标计算功能。int[][] gameMap存储每个格子的图片编号,0表示空格子,非0值表示对应编号的图片。Point类型的变量firstSelect、secondSelect存储玩家选中的两个格子的坐标。List存储加载的图片资源,索引与图片编号对应。int类型变量currentSteps(当前步数)、remainingTime(剩余时间)存储游戏进度数据。界面采用BorderLayout布局管理器,分为三个部分:
FlowLayout布局保证元素排列整齐。核心算法是判断两个图片是否可连接,支持三种连接方式:直连、单拐点、双拐点,算法流程如下:
判断两个点是否在同一行或同一列,且中间的格子均为空(图片编号为0)。
寻找两个点的拐点(候选拐点为(p1.x, p2.y)和(p2.x, p1.y)),判断拐点是否为空,且拐点与两个点分别直连。
遍历所有空格子作为第一个拐点,判断该拐点与第一个点直连,且存在第二个拐点与第一个拐点和第二个点分别直连(复用单拐点判断逻辑)。
遍历所有非空格子的坐标,两两配对检测是否存在编号相同且可连接的图片对:
posList中。false(非死局);否则返回true(死局)。打乱现有非空图片的布局,保留空格,保证游戏连续性:
Collections.shuffle()方法打乱图片编号列表。| 测试项 | 配置信息 |
|---|---|
| 操作系统 | Windows 10/11、macOS、Linux(Ubuntu) |
| Java版本 | JDK 8、JDK 11、JDK 17(兼容) |
| 开发工具 | IntelliJ IDEA、Eclipse、VS Code |
| 屏幕分辨率 | 1920×1080及以上(推荐) |
| 测试用例编号 | 测试功能 | 测试步骤 | 预期结果 |
|---|---|---|---|
| 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. 启动游戏 | 游戏正常运行,使用随机颜色块替代图片 |
/** * 判断两个点是否可连接(直连、单拐点、双拐点) * @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; }/** * 死局检测:判断是否存在至少一对可连接的图片 * @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(); } } }/** * 处理选中逻辑(新增步数统计、失败判断、死局检测) * @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); } }/** * 游戏面板,负责绘制游戏元素(优化选中样式、连线、悬浮效果) */ 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)); } }