JSP+Java Web实战:从棋盘算法到部署,完整复现连连看创新项目

发布时间:2026/10/1 3:13:03
JSP+Java Web实战:从棋盘算法到部署,完整复现连连看创新项目 说实话“JAVA-JSP创新项目-连连看”这种题目几乎每个学Java的人都绕不过去。你说它是个网页游戏吧它确实就是浏览器里打开一个JSP页面点格子消除图标你说它是Java Web项目吧它又确实把Session、Servlet、JDBC、JavaBean这些东西全串起来了。我这些年带过不少学生做课设也在公司内部用JSP搭过几次小工具这个题目算是Java Web方向里性价比很高的一款——代码量不大但该考的考点都能覆盖做完了还能真玩不至于像“学生管理系统”那样做完自己都不想打开。这篇文章就以我实际做过的方案为准把整个项目从思路拆解到核心算法、从前端交互到部署打包完整展开讲。目标是让正在做课设、毕设或者想拿Java练手的朋友看完之后能照着重现一遍并且在答辩或面试时能讲清楚“我为什么这么设计”。1. 项目整体设计与技术选型1.1 这个项目到底在练什么很多人一看“连连看”就觉得是纯前端游戏用HTMLJS就能写何必扯上JSP和Java。这个想法没错但那是“网页版连连看”不是“JSP创新项目”。作为Java Web方向的课程设计评分的重点从来不是游戏多好玩而是你有没有把Java Web的核心知识点用起来。JSP在这个项目里的定位是“动态页面渲染”。棋盘长什么样、格子里面放什么图这些不是写死在HTML里的而是后端Java算出数据后通过JSP语法动态生成表格。用户点击格子后前端把坐标传给后台Java核心算法判断两个格子能不能消除再把结果返回前端由JavaScript更新界面。这样一个流程走下来JSP负责展示逻辑Java负责业务逻辑职责分得清清楚楚。这个项目真正训练的能力有几块面向对象建模棋盘、格子、游戏状态、集合框架的使用List、Map存数据和配对、Servlet与JSP数据交互、Session跟踪用户状态、JDBC做登录注册和排行榜、以及经典的算法逻辑路径搜索和碰撞检测。把这些都想明白这个项目就算吃透了。1.2 功能拆解与技术栈选择我建议项目至少包含这几个功能模块用户登录注册历史最高分保存到数据库游戏主界面棋盘展示、计时、得分、剩余图标对数统计核心消除逻辑点击两个图案相同且路径连通的格子完成消除辅助功能提示hint、重排reshuffle、暂停输赢判定全部消除则胜利死局则提示重排。技术栈上我的固定搭配是JDK 1.8或更高版本 Tomcat 9 MySQL 5.7/8.0 JSP/Servlet原生技术前端只用基础JavaScript和CSS顶多引入jQuery简化DOM操作。不用Spring Boot的原因很简单课设阶段没必要上框架而且很多学校的Java Web课程只教到JSP/Servlet框架用得太超前反而容易被答辩老师追问到尴尬。还有个问题是“JSP到底要不要大量写Java代码”。我的建议是JSP页面里尽量少写业务代码Java逻辑全部放到后台类和Servlet里处理。JSP只负责用EL表达式、JSTL和简单脚本输出数据。这样不仅代码干净答辩时还能主动说一句“我做了前后端逻辑分离”加分效果实实在在。2. 连连看核心算法实现这个项目的灵魂就是核心算法直接决定了程序好不好玩、代码值不值得加分。我把它拆成三块来讲棋盘生成、路径连通判定、死局检测与重排。2.1 棋盘生成成对布局与洗牌连连看的棋盘是一个N行M列的二维网格每个格子里放一种图标每种图标必须出现偶数次保证最后能全部消完。我最开始做的时候直接随机摆放结果经常出现“剩下最后两个但死活对不上”的情况因为数量奇偶没控制好。所以正确做法是先构造一个成对出现的数组再打乱顺序填入棋盘。这里有个容易忽略的点外层留一圈空白。比如游戏区域是6行8列我在Java里实际用的是一个8行10列的二维数组下标0和最后一行一列都空着。这样做的好处后面会体现——边缘上的方块也可以借助外部空白区域连通。留了这圈虚拟空白通道边缘消除的规则就好处理很多不用写一堆边界特判。public class BoardGenerator { private int rows; private int cols; private int[][] board; public BoardGenerator(int rows, int cols) { this.rows rows; this.cols cols; // 外层加一圈0所以实际数组行列各加2 this.board new int[rows 2][cols 2]; } public int[][] generate(int iconTypes) { // 确保每种图标成对出现 int cellCount rows * cols; if (cellCount % 2 ! 0) { throw new IllegalArgumentException(棋盘格子数必须为偶数); } int[] icons new int[cellCount]; for (int i 0; i cellCount / 2; i) { icons[2 * i] i % iconTypes 1; icons[2 * i 1] i % iconTypes 1; } // Fisher-Yates 洗牌 Random random new Random(); for (int i icons.length - 1; i 0; i--) { int j random.nextInt(i 1); int tmp icons[i]; icons[i] icons[j]; icons[j] tmp; } // 填入棋盘内层 int index 0; for (int i 1; i rows; i) { for (int j 1; j cols; j) { board[i][j] icons[index]; } } return board; } }洗牌算法用的Fisher-Yates这是目前最靠谱的等概率随机排列算法。别用Collections.shuffle代替手动实现吗其实也行但自己写一遍能加深理解而且面试时经常被问到课设里展示一下是加分项。2.2 消除路径判定从直线到双拐角连连看的核心规则是两个格子图案相同、且最多经过两个转折点就能连通时才允许消除。换句话说两个格子之间要存在一条“最多转两次弯”的路径且路径上所有格子除起点终点外都是空的。这个判定过程可以从简单到复杂拆成三层第一层直线连通。两个格子在同一个方向同行或同列中间没有障碍物。比如(2,3)和(2,7)在同一行检查(2,4)到(2,6)是否全为空即可。这是最基础的情况。第二层一个拐角连通。两个格子不在同一行也不在同一列但能通过一个拐角点连通。拐角点就是两个候选坐标(x1, y2)和(x2, y1)。只要这两个坐标的格子是空的并且从起点到拐角点、从拐角点到终点分别都是直线连通就说明成立。第三层两个拐角连通。这是最容易漏的场景。比如起点在左上、终点在右下路径可能需要先向右走、再向下、再向右形成两个拐角。判断方法是扫描起点所在的行和列找出所有与起点直线连通的空格子以这些格子为中转点再用“一个拐角”的逻辑去尝试连接终点。换个理解方式在起点可能到达的所有位置中有没有一个位置能通过单拐角连到终点。public boolean canConnect(int[][] board, int x1, int y1, int x2, int y2) { if (board[x1][y1] ! board[x2][y2]) return false; if (x1 x2 y1 y2) return false; int value board[x1][y1]; board[x1][y1] board[x2][y2] 0; // 临时置空便于路径判断 boolean result false; // 1. 直线连通 if (x1 x2 || y1 y2) { result isLineClear(board, x1, y1, x2, y2); } // 2. 一个拐角 if (!result isCornerClear(board, x1, y1, x2, y2)) { result true; } // 3. 两个拐角 if (!result) { result hasTwoTurnPath(board, x1, y1, x2, y2); } board[x1][y1] board[x2][y2] value; return result; }isLineClear就是沿同一行或同一列遍历中间全是0才返回true。hasTwoTurnPath的实现思路是枚举起点能直线到达的每个空格再判断该空格能否通过一个拐角连到终点。注意遍历时要包括外圈那层为0的区域这正是棋盘设计时多留一圈的原因。private boolean hasTwoTurnPath(int[][] board, int x1, int y1, int x2, int y2) { int rows board.length; int cols board[0].length; // 扫描起点所在的列向下向上 for (int i 0; i rows; i) { if (i x1) continue; if (board[i][y1] 0 isLineClear(board, x1, y1, i, y1) cornerConnect(board, i, y1, x2, y2)) { return true; } } // 扫描起点所在的行向左向右 for (int j 0; j cols; j) { if (j y1) continue; if (board[x1][j] 0 isLineClear(board, x1, y1, x1, j) cornerConnect(board, x1, j, x2, y2)) { return true; } } return false; }这个算法的复杂度是O(n*m)棋盘规模不大时性能完全够用。很多网上的实现还额外判断了“两条直线同时可走”的路径但如果拐角点本身被占就不用继续探测了我们的写法已经把这类情况过滤掉了逻辑更简洁。2.3 死局检测与自动重排游戏进行到中后期经常出现“明明剩余图标很多但就是找不到一对能连通的”这就是死局。死局不处理玩家只能退出重开体验很差。所以要在每次消除后检查一次当前棋盘上是否还存在可消除的对。检测方法简单粗暴但可靠遍历所有未消除的格子嵌套两层循环对每对图案相同的格子调用canConnect能找到任意一对就说明局面还没死。如果全部检查完都找不到就执行重排。public boolean hasDeadLock(int[][] board) { Listint[] positions new ArrayList(); for (int i 1; i board.length - 1; i) { for (int j 1; j board[i].length - 1; j) { if (board[i][j] ! 0) positions.add(new int[]{i, j}); } } // 用Map按图标值分组减少无效配对次数 MapInteger, Listint[] map new HashMap(); for (int[] pos : positions) { map.computeIfAbsent(board[pos[0]][pos[1]], k - new ArrayList()).add(pos); } for (Listint[] list : map.values()) { for (int i 0; i list.size(); i) { for (int j i 1; j list.size(); j) { int[] a list.get(i); int[] b list.get(j); if (canConnect(board, a[0], a[1], b[0], b[1])) { return false; } } } } return true; }重排的做法是把所有剩余图标的值收集起来仍是成对数组再次洗牌后填回棋盘。这里有个细节重排后需要再检查一次是否还死局如果连续多次仍然死局极端情况就把整个棋盘重新洗牌。实测中连续重排两次基本都能解开不用做太复杂的回溯处理。3. 从Java核心到JSP页面的完整实现算法搞定之后剩下的是怎么把算法挂到Web页面上去。这个环节牵扯到的地方很多我按流程逐个讲。3.1 登录注册与数据库访问虽然连连看可以做成免登录的但加上用户系统之后项目档次立刻不一样。我在这个项目里用了一张users表字段包括用户ID、用户名、密码、最高分、游戏局数。CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, highest_score INT DEFAULT 0, play_count INT DEFAULT 0 );数据库连接我直接用JDBC原生的DriverManager没有套连接池。课设阶段连接池不是必需项但代码要写成独立的工具类方便以后换成C3P0或者Druid。另外登录校验一定要用PreparedStatement防止SQL注入。很多同学在这上面犯懒直接拼接字符串答辩时被老师问一下“SQL注入怎么防”就哑火了。public User login(String username, String password) { String sql SELECT * FROM users WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getString(username), rs.getInt(highest_score)); } } } catch (SQLException e) { e.printStackTrace(); } return null; }密码存明文在课设里比较常见但如果你想让项目亮眼用MD5或者SHA-256做一次哈希再入库并在用户注册时加一个随机盐salt讲解起来会显得很专业。加盐的做法不复杂hash(password salt)盐值每次注册随机生成并单独存一列校验时取出盐再算一次比对即可。3.2 JSP项目目录结构与界面渲染用IDEA新建JSP项目我推荐用Maven的maven-archetype-webapp骨架目录结构清晰后面打包war也方便。核心目录如下src/main/java/com/example/llk/ ├── entity/User.java ├── dao/UserDao.java ├── util/DBUtil.java ├── game/BoardGenerator.java ├── game/GameCore.java └── servlet/LoginServlet.java, InitGameServlet.java, ActionServlet.java src/main/webapp/ ├── login.jsp ├── game.jsp ├── css/style.css ├── js/game.js └── images/icon1.png ...game.jsp的核心逻辑是显示棋盘。我是这么做的进入游戏时InitGameServlet生成一个棋盘对象存进Session然后把棋盘数据通过请求转发传到JSP。JSP用c:forEach双重循环输出表格每个单元格的ID用行列号拼接比如cell_3_5这样JavaScript拿到格子后能直接通过ID定位。table idboard c:forEach vari begin1 end${rows} tr c:forEach varj begin1 end${cols} td idcell_${i}_${j}>async function handleClick(cell) { if (cell.querySelector(img) null) return; // 已消除 if (firstCell ! null secondCell ! null) return; // 等待处理中 if (firstCell null) { firstCell cell; cell.classList.add(selected); } else { secondCell cell; let x1 firstCell.dataset.x, y1 firstCell.dataset.y; let x2 cell.dataset.x, y2 cell.dataset.y; let resp await fetch(action?x1 x1 y1 y1 x2 x2 y2 y2); let data await resp.json(); if (data.success) { firstCell.innerHTML ; secondCell.innerHTML ; updateScore(data.score); } else { firstCell.classList.remove(selected); } firstCell null; secondCell null; } }后端处理逻辑比较直接ActionServlet从Session取出棋盘数组调用GameCore.canConnect()成功则把两个格子置为0同时更新剩余对数失败则返回失败标记。注意每次校验后还要调用hasDeadLock()检查死局如果死局则直接返回needShuffle前端收到后刷新页面或者重新请求棋盘数据。有一个热词提到“jsp页面让加载完后刷新一次”这个场景在连连看里确实存在。比如用户按F5刷新页面如果不清空Session中的游戏状态会出现一个完全随机的老棋盘点击会报错。我建议在game.jsp页面顶部的window.onload里做一次判断如果需要重新开局就直接跳转到initGame请求保证进入永远是新游戏。3.4 war包打包与Tomcat部署部署环节手把手再说一遍这里翻车的概率不低。项目做完后用IDEA右侧Maven面板执行clean package如果项目结构正确且没有依赖问题会在target目录下生成.war文件。把这个war文件复制到Tomcat的webapps目录启动Tomcat它会自动解压并部署。一个常见的坑是web.xml的配置。Servlet 3.0以上可以用注解WebServlet(/action)代替web.xml配置但如果你的项目还配置了JSP欢迎页一定要确认welcome-file指向的是login.jsp否则访问根路径会404。welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list数据库配置我习惯单独抽一个db.properties放在src/main/resources下用DBUtil加载。注意Tomcat运行时的classpath已经包含target/classes所以配置文件不需要额外复制到webapp里。4. 常见问题与独家排错笔记最后这部分是纯实战经验汇总全是自己踩过的坑整理出来给后来人避雷。4.1 问题速查表现象常见原因解决方案JSP页面中文乱码页面编码与Tomcat默认编码不一致所有JSP头部统一用pageEncodingUTF-8并在Servlet里设置request.setCharacterEncoding(UTF-8)图片加载不出来相对路径引用错误用%request.getContextPath()%/images/icon_1.png拼绝对路径点击格子没有反应前端坐标与后端二维数组行列搞反统一按“先行后列”定义前端>