diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md deleted file mode 100644 index f09d98d..0000000 --- a/.gitee/ISSUE_TEMPLATE.zh-CN.md +++ /dev/null @@ -1,13 +0,0 @@ -### 该问题是怎么引起的? - - - -### 重现步骤 - - - -### 报错信息 - - - - diff --git a/.gitignore b/.gitignore index 00e1a7d..7f7af37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.idea/ +# Maven target/ pom.xml.tag pom.xml.releaseBackup @@ -8,5 +8,59 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +.settings/ +.classpath +.project +.factorypath + +# Spring Boot +*.log +logs/ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Java +*.class +*.jar +*.war +*.ear +hs_err_pid* +replay_pid* + +# Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath + +# NetBeans +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +# Application +application-local.yml +application-dev.yml +application-prod.yml diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 2d459a0..0000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# draw-and-guess - -#### Description -你画我猜 SpringBoot Websocket - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index e46c042..7bff596 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,117 @@ 你画我猜 SpringBoot Websocket #### 软件架构 -- 后端:Java21、Maven、SpringBoot +- 后端:Java21、Maven、SpringBoot 3.2.4 - 前端:HTML + jQuery - 通讯:HTTP + Websocket +- 工具库:Lombok、Hutool、FastJSON2 + +#### 项目结构 +``` +draw-and-guess/ +├── src/main/java/cn/airnan/dg/ +│ ├── configs/ # 配置类(CORS、WebSocket、全局配置) +│ ├── controller/ # 控制器(玩家、房间、题目、首页) +│ ├── entity/ # 实体类(玩家、房间、画板、画笔、点、题目) +│ ├── pojo/ # 数据传输对象 +│ ├── service/ # 业务逻辑层 +│ └── utils/ # 工具类(WebSocket、消息封装、通用响应) +├── src/main/resources/ +│ ├── static/ # 静态资源(HTML、CSS、JS、图标) +│ ├── application.yml # 应用配置 +│ └── topic.json # 题库文件 +└── pom.xml # Maven 依赖配置 +``` #### 安装教程 -1. 修改前端`index.html`中的后端地址 -2. 运行命令: +1. **修改前端配置** + 打开 `src/main/resources/static/index.html`,修改 `backendAddr` 变量为实际的服务器地址: + ```javascript + const backendAddr = "192.168.31.222:8080" // 修改为你的服务器IP和端口 ``` + +2. **编译打包** + ```bash + # 使用 Maven Wrapper 编译打包 + mvnw clean package + + # 或使用本地 Maven + mvn clean package + ``` + 打包完成后,在 `target` 目录下会生成 `draw-and-guess-0.0.1-SNAPSHOT.jar` + +3. **运行应用** + ```bash # 直接运行 - java -jar xxx.jar - # 后台运行 - nohup java -jar draw-and-guess-0.0.1-SNAPSHOT.jar >> ./draw.log 2>&1 & + java -jar target/draw-and-guess-0.0.1-SNAPSHOT.jar + + # 后台运行(Linux/Mac) + nohup java -jar target/draw-and-guess-0.0.1-SNAPSHOT.jar >> ./draw.log 2>&1 & + + # Windows 后台运行 + start /B java -jar target/draw-and-guess-0.0.1-SNAPSHOT.jar ``` -3. 访问`http://IP:8080`进行游玩 + +4. **访问应用** + 打开浏览器访问 `http://IP:8080` 进行游玩 #### 使用说明 -1. 题库在`resources/topic.json`中,可自行添加。 +1. **创建玩家** + - 在首页输入玩家昵称 + - 点击"创建"按钮注册玩家 + +2. **房间管理** + - 查看房间列表,点击"刷新列表"获取最新房间 + - 点击"创建房间"创建新房间并成为房主 + - 点击"加入房间"加入已有房间 + +3. **游戏玩法** + - 进入房间后,玩家可以实时聊天 + - 房主或其他玩家可以在画板上绘画 + - 支持多种颜色、画笔粗细调节 + - 支持橡皮擦功能 + - 可以清空画板重新绘画 + +4. **题库配置** + - 题库文件位于 `resources/topic.json` + - 格式为 JSON 数组,每个元素包含题目和提示 + - 可自行添加或修改题目: + ```json + [ + {"题目名称":"提示信息"}, + {"空调":"日常物品、2个字"} + ] + ``` + +#### API 接口说明 + +**玩家接口** +- `POST /api/player/create` - 创建玩家 +- `GET /api/player/list` - 获取玩家列表 +- `POST /api/player/isExist` - 检查玩家是否存在 + +**房间接口** +- `GET /api/room/list` - 获取房间列表 +- `POST /api/room/create` - 创建房间 +- `POST /api/room/canJoin` - 检查是否可加入房间 + +**题目接口** +- `GET /api/topic/random` - 获取随机题目 + +**WebSocket 连接** +- 连接地址:`ws://IP:8080/websocket/{playerId}/{roomId}` +- 消息类型: + - `room-msg` - 房间通知消息 + - `room-chat` - 聊天消息 + - `room-draw` - 绘画数据 + - `room-draw-clear` - 清空画板 + +#### 技术特点 +- 基于 Spring Boot 3.2.4 + Java 21 开发 +- 使用 WebSocket 实现实时双向通信 +- 支持多人在线绘画和聊天 +- 前端使用 Canvas 实现绘画功能 +- 支持移动端和 PC 端访问 diff --git a/src/main/java/cn/airnan/dg/configs/Global.java b/src/main/java/cn/airnan/dg/configs/Global.java index 3a67738..1b3dc05 100644 --- a/src/main/java/cn/airnan/dg/configs/Global.java +++ b/src/main/java/cn/airnan/dg/configs/Global.java @@ -31,6 +31,61 @@ public class Global { */ public static final String MSG_TYPE_ROOM_MSG = "room-msg"; + /** + * 消息类型-游戏开始 + */ + public static final String MSG_TYPE_GAME_START = "game-start"; + + /** + * 消息类型-游戏结束 + */ + public static final String MSG_TYPE_GAME_END = "game-end"; + + /** + * 消息类型-轮次切换 + */ + public static final String MSG_TYPE_ROUND_CHANGE = "round-change"; + + /** + * 消息类型-题目分配 + */ + public static final String MSG_TYPE_TOPIC_ASSIGN = "topic-assign"; + + /** + * 消息类型-答题正确 + */ + public static final String MSG_TYPE_ANSWER_CORRECT = "answer-correct"; + + /** + * 消息类型-答题错误 + */ + public static final String MSG_TYPE_ANSWER_WRONG = "answer-wrong"; + + /** + * 消息类型-分数更新 + */ + public static final String MSG_TYPE_SCORE_UPDATE = "score-update"; + + /** + * 消息类型-倒计时 + */ + public static final String MSG_TYPE_COUNTDOWN = "countdown"; + + /** + * 消息类型-绘画者切换 + */ + public static final String MSG_TYPE_DRAWER_CHANGE = "drawer-change"; + + /** + * 消息类型-绘画权限控制 + */ + public static final String MSG_TYPE_DRAW_PERMISSION = "draw-permission"; + + /** + * 消息类型-游戏状态更新 + */ + public static final String MSG_TYPE_GAME_STATE = "game-state"; + /** * 题库 */ diff --git a/src/main/java/cn/airnan/dg/configs/WebSocketConfig.java b/src/main/java/cn/airnan/dg/configs/WebSocketConfig.java new file mode 100644 index 0000000..5cff9c4 --- /dev/null +++ b/src/main/java/cn/airnan/dg/configs/WebSocketConfig.java @@ -0,0 +1,22 @@ +package cn.airnan.dg.configs; + +import cn.airnan.dg.service.GameService; +import cn.airnan.dg.utils.WebSocket; +import org.springframework.context.annotation.Configuration; + +import jakarta.annotation.PostConstruct; + +@Configuration +public class WebSocketConfig { + + private final GameService gameService; + + public WebSocketConfig(GameService gameService) { + this.gameService = gameService; + } + + @PostConstruct + public void init() { + WebSocket.setGameService(gameService); + } +} diff --git a/src/main/java/cn/airnan/dg/controller/GameController.java b/src/main/java/cn/airnan/dg/controller/GameController.java new file mode 100644 index 0000000..28fac2f --- /dev/null +++ b/src/main/java/cn/airnan/dg/controller/GameController.java @@ -0,0 +1,89 @@ +package cn.airnan.dg.controller; + +import cn.airnan.dg.configs.Global; +import cn.airnan.dg.service.GameService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/game") +public class GameController { + + @Autowired + private GameService gameService; + + @PostMapping("/start") + public Map startGame(@RequestBody Map request) { + String roomId = request.get("roomId"); + String playerId = request.get("playerId"); + + Map response = new HashMap<>(); + + if (!Global.ROOMS.containsKey(roomId)) { + response.put("success", false); + response.put("message", "房间不存在"); + return response; + } + + if (!Global.ROOMS.get(roomId).getMaster().equals(playerId)) { + response.put("success", false); + response.put("message", "只有房主可以开始游戏"); + return response; + } + + if (Global.ROOMS.get(roomId).getPlayers().size() < 2) { + response.put("success", false); + response.put("message", "至少需要2名玩家才能开始游戏"); + return response; + } + + try { + gameService.startGame(roomId); + response.put("success", true); + response.put("message", "游戏开始"); + } catch (Exception e) { + log.error("开始游戏失败", e); + response.put("success", false); + response.put("message", "游戏开始失败"); + } + + return response; + } + + @PostMapping("/end") + public Map endGame(@RequestBody Map request) { + String roomId = request.get("roomId"); + String playerId = request.get("playerId"); + + Map response = new HashMap<>(); + + if (!Global.ROOMS.containsKey(roomId)) { + response.put("success", false); + response.put("message", "房间不存在"); + return response; + } + + if (!Global.ROOMS.get(roomId).getMaster().equals(playerId)) { + response.put("success", false); + response.put("message", "只有房主可以结束游戏"); + return response; + } + + try { + gameService.endGame(roomId); + response.put("success", true); + response.put("message", "游戏结束"); + } catch (Exception e) { + log.error("结束游戏失败", e); + response.put("success", false); + response.put("message", "游戏结束失败"); + } + + return response; + } +} diff --git a/src/main/java/cn/airnan/dg/entity/Player.java b/src/main/java/cn/airnan/dg/entity/Player.java index f29ba22..39e61eb 100644 --- a/src/main/java/cn/airnan/dg/entity/Player.java +++ b/src/main/java/cn/airnan/dg/entity/Player.java @@ -35,19 +35,24 @@ public class Player { */ private Integer score = 0; + /** + * 本轮是否已猜对 + */ + private boolean hasGuessed = false; + /** * 加分 * * @param add 要增加的分数 */ - private void addScore(Integer add) { + public void addScore(Integer add) { this.score += add; } /** * 重置分数 */ - private void clearScore() { + public void clearScore() { this.score = 0; } diff --git a/src/main/java/cn/airnan/dg/entity/Room.java b/src/main/java/cn/airnan/dg/entity/Room.java index 71dfee7..0f87592 100644 --- a/src/main/java/cn/airnan/dg/entity/Room.java +++ b/src/main/java/cn/airnan/dg/entity/Room.java @@ -44,6 +44,16 @@ public class Room { */ private Topic topic; + /** + * 游戏题目列表 + */ + private List topics = new ArrayList<>(); + + /** + * 当前题目 + */ + private Topic currentTopic; + /** * 游戏开始状态 */ @@ -54,6 +64,36 @@ public class Room { */ private Integer countDown = 0; + /** + * 当前绘画者ID + */ + private String currentDrawer; + + /** + * 当前轮次 + */ + private Integer currentRound = 0; + + /** + * 总轮次 + */ + private Integer totalRounds = 5; + + /** + * 游戏状态:0-准备,1-绘画,2-结算 + */ + private Integer gameState = 0; + + /** + * 本轮已猜对人数 + */ + private Integer guessedCount = 0; + + /** + * 绘画时间(秒) + */ + private Integer drawTime = 60; + /** * 添加玩家 * @@ -104,4 +144,64 @@ public class Room { } + /** + * 获取下一个绘画者 + * + * @return 下一个绘画者ID + */ + public String getNextDrawer() { + if (players.isEmpty()) { + return null; + } + if (currentDrawer == null) { + return players.get(0).getId(); + } + for (int i = 0; i < players.size(); i++) { + if (players.get(i).getId().equals(currentDrawer)) { + int nextIndex = (i + 1) % players.size(); + return players.get(nextIndex).getId(); + } + } + return players.get(0).getId(); + } + + /** + * 检查玩家是否是当前绘画者 + * + * @param playerId 玩家ID + * @return 是否是绘画者 + */ + public boolean isCurrentDrawer(String playerId) { + return currentDrawer != null && currentDrawer.equals(playerId); + } + + /** + * 重置游戏状态 + */ + public void resetGame() { + this.currentRound = 0; + this.gameState = 0; + this.currentDrawer = null; + this.guessedCount = 0; + this.countDown = 0; + for (Player player : players) { + player.clearScore(); + } + } + + /** + * 获取玩家对象 + * + * @param playerId 玩家ID + * @return 玩家对象 + */ + public Player getPlayer(String playerId) { + for (Player player : players) { + if (player.getId().equals(playerId)) { + return player; + } + } + return null; + } + } diff --git a/src/main/java/cn/airnan/dg/service/GameService.java b/src/main/java/cn/airnan/dg/service/GameService.java new file mode 100644 index 0000000..0523f53 --- /dev/null +++ b/src/main/java/cn/airnan/dg/service/GameService.java @@ -0,0 +1,258 @@ +package cn.airnan.dg.service; + +import cn.airnan.dg.configs.Global; +import cn.airnan.dg.entity.Player; +import cn.airnan.dg.entity.Room; +import cn.airnan.dg.entity.Topic; +import cn.airnan.dg.utils.WebSocket; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class GameService { + + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10); + + private static final Map countdownTasks = new HashMap<>(); + + private final TopicService topicService; + + public GameService(TopicService topicService) { + this.topicService = topicService; + } + + public void startGame(String roomId) { + Room room = Global.ROOMS.get(roomId); + if (room == null || room.isBegin()) { + return; + } + + room.setBegin(true); + room.setCurrentRound(0); + room.setGameState(0); + room.getPlayers().forEach(Player::clearScore); + + List topics = topicService.randomTopics(room.getTotalRounds()); + room.setTopics(topics); + + sendGameStartMessage(room); + startNextRound(room); + } + + public void endGame(String roomId) { + Room room = Global.ROOMS.get(roomId); + if (room == null) { + return; + } + + room.setBegin(false); + room.setGameState(0); + stopCountdown(roomId); + + Map data = new HashMap<>(); + data.put("roomId", roomId); + data.put("winner", getWinner(room)); + data.put("finalScores", getFinalScores(room)); + + WebSocket.sendToRoom(roomId, Global.MSG_TYPE_GAME_END, data); + } + + public void startNextRound(Room room) { + if (room.getCurrentRound() >= room.getTotalRounds()) { + endGame(room.getId()); + return; + } + + room.setCurrentRound(room.getCurrentRound() + 1); + room.setGameState(1); + room.setGuessedCount(0); + room.getPlayers().forEach(p -> p.setHasGuessed(false)); + + String nextDrawer = room.getNextDrawer(); + room.setCurrentDrawer(nextDrawer); + + Topic currentTopic = room.getTopics().get(room.getCurrentRound() - 1); + room.setCurrentTopic(currentTopic); + + sendRoundChangeMessage(room); + sendDrawerChangeMessage(room); + sendTopicAssignMessage(room); + sendDrawPermissionMessage(room); + startCountdown(room); + } + + public boolean checkAnswer(String roomId, String playerId, String answer) { + Room room = Global.ROOMS.get(roomId); + if (room == null || room.getGameState() != 1) { + return false; + } + + Player player = room.getPlayer(playerId); + if (player == null || player.isHasGuessed() || player.getId().equals(room.getCurrentDrawer())) { + return false; + } + + Topic currentTopic = room.getCurrentTopic(); + if (currentTopic == null) { + return false; + } + + if (currentTopic.getName().equalsIgnoreCase(answer.trim())) { + player.setHasGuessed(true); + room.setGuessedCount(room.getGuessedCount() + 1); + + int score = calculateScore(room); + player.addScore(score); + + sendAnswerCorrectMessage(room, player, score); + sendScoreUpdateMessage(room); + + if (room.getGuessedCount() >= room.getPlayers().size() - 1) { + scheduler.schedule(() -> startNextRound(room), 2, TimeUnit.SECONDS); + } + + return true; + } else { + sendAnswerWrongMessage(room, player); + return false; + } + } + + private int calculateScore(Room room) { + int baseScore = 100; + int timeBonus = room.getDrawTime() / 2; + return baseScore + timeBonus; + } + + private Player getWinner(Room room) { + return room.getPlayers().stream() + .max(Comparator.comparingInt(Player::getScore)) + .orElse(null); + } + + private Map getFinalScores(Room room) { + Map scores = new HashMap<>(); + room.getPlayers().forEach(p -> scores.put(p.getId(), p.getScore())); + return scores; + } + + private void startCountdown(Room room) { + stopCountdown(room.getId()); + + Runnable countdownTask = new CountdownTask(room, this); + countdownTasks.put(room.getId(), countdownTask); + scheduler.scheduleAtFixedRate(countdownTask, 1, 1, TimeUnit.SECONDS); + } + + private void stopCountdown(String roomId) { + Runnable task = countdownTasks.remove(roomId); + if (task instanceof CountdownTask) { + ((CountdownTask) task).stop(); + } + } + + private void sendGameStartMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("totalRounds", room.getTotalRounds()); + data.put("drawTime", room.getDrawTime()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_GAME_START, data); + } + + private void sendRoundChangeMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("currentRound", room.getCurrentRound()); + data.put("totalRounds", room.getTotalRounds()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_ROUND_CHANGE, data); + } + + private void sendDrawerChangeMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("drawerId", room.getCurrentDrawer()); + data.put("drawerName", room.getPlayer(room.getCurrentDrawer()).getId()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_DRAWER_CHANGE, data); + } + + private void sendTopicAssignMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("topic", room.getCurrentTopic()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_TOPIC_ASSIGN, data); + } + + private void sendDrawPermissionMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("drawerId", room.getCurrentDrawer()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_DRAW_PERMISSION, data); + } + + private void sendAnswerCorrectMessage(Room room, Player player, int score) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("playerId", player.getId()); + data.put("playerName", player.getId()); + data.put("score", score); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_ANSWER_CORRECT, data); + } + + private void sendAnswerWrongMessage(Room room, Player player) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("playerId", player.getId()); + data.put("playerName", player.getId()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_ANSWER_WRONG, data); + } + + private void sendScoreUpdateMessage(Room room) { + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("scores", getFinalScores(room)); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_SCORE_UPDATE, data); + } + + private static class CountdownTask implements Runnable { + private final Room room; + private final GameService gameService; + private volatile boolean running = true; + + public CountdownTask(Room room, GameService gameService) { + this.room = room; + this.gameService = gameService; + } + + @Override + public void run() { + if (!running) { + return; + } + + if (room.getDrawTime() > 0) { + room.setDrawTime(room.getDrawTime() - 1); + + Map data = new HashMap<>(); + data.put("roomId", room.getId()); + data.put("timeLeft", room.getDrawTime()); + WebSocket.sendToRoom(room.getId(), Global.MSG_TYPE_COUNTDOWN, data); + } else { + stop(); + scheduler.schedule(() -> gameService.startNextRound(room), 1, TimeUnit.SECONDS); + } + } + + public void stop() { + running = false; + } + } +} diff --git a/src/main/java/cn/airnan/dg/utils/WebSocket.java b/src/main/java/cn/airnan/dg/utils/WebSocket.java index 7f8e964..7205110 100644 --- a/src/main/java/cn/airnan/dg/utils/WebSocket.java +++ b/src/main/java/cn/airnan/dg/utils/WebSocket.java @@ -3,6 +3,7 @@ package cn.airnan.dg.utils; import cn.airnan.dg.configs.Global; import cn.airnan.dg.entity.Player; import cn.airnan.dg.entity.Room; +import cn.airnan.dg.service.GameService; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.TypeReference; import jakarta.websocket.OnClose; @@ -21,6 +22,12 @@ import java.util.Map; @ServerEndpoint("/websocket/{playerId}/{roomId}") public class WebSocket { + private static GameService gameService; + + public static void setGameService(GameService gameService) { + WebSocket.gameService = gameService; + } + /** * 加入房间时触发WebSocket事件 @@ -105,6 +112,13 @@ public class WebSocket { // 解析消息为 SocketMsg 结构体 SocketMsg socketMsg = JSON.parseObject(message, new TypeReference<>() { }); + + // 处理答题消息 + if ("room-chat-answer".equals(socketMsg.getType())) { + handleAnswer(socketMsg.getRoomId(), socketMsg.getPlayerId(), socketMsg.getData()); + return; + } + // 匹配房间并转发消息 Global.ROOMS.get(socketMsg.getRoomId()).sendMessage(socketMsg.getType(), socketMsg.getPlayerId(), socketMsg.getData()); } catch (Exception e) { @@ -112,6 +126,66 @@ public class WebSocket { } } + private void handleAnswer(String roomId, String playerId, String answer) { + Room room = Global.ROOMS.get(roomId); + if (room == null) { + return; + } + + // 使用 GameService 检查答案 + boolean isCorrect = gameService.checkAnswer(roomId, playerId, answer); + + if (isCorrect) { + // 答对了,发送正确消息 + String message = playerId + " 猜对了!"; + room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message); + } else { + // 答错了,发送错误消息 + String message = playerId + " 猜错了"; + room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message); + } + } + + private boolean checkAnswer(Room room, String playerId, String answer) { + if (room.getCurrentTopic() == null) { + return false; + } + + return room.getCurrentTopic().getName().equalsIgnoreCase(answer.trim()); + } + + /** + * 发送消息到指定房间 + * + * @param roomId 房间ID + * @param type 消息类型 + * @param data 消息数据 + */ + public static void sendToRoom(String roomId, String type, Object data) { + Room room = Global.ROOMS.get(roomId); + if (room == null) { + return; + } + + SocketMsg socketMsg = new SocketMsg<>(); + socketMsg.setType(type); + socketMsg.setRoomId(roomId); + socketMsg.setData(data); + + String message = JSON.toJSONString(socketMsg); + for (Player player : room.getPlayers()) { + if (player.getSession() != null && player.getSession().isOpen()) { + try { + synchronized (player.getSession()) { + player.getSession().getAsyncRemote().sendText(message); + } + } catch (Exception e) { + log.error("发送房间消息失败:{}", e.getMessage()); + } + } + } + } + /** * 此为广播消息 * diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 732d517..dd2c175 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -41,11 +41,15 @@
+
+
第 0 / 5 轮
+
倒计时:60秒
+
-
题目:坐井观天
-
提示:成语,四个字
+
题目:等待游戏开始
+
提示:--
@@ -108,11 +112,11 @@
-
张
-
王
-
-
-
+
张
+
王
+
+
+
@@ -121,6 +125,10 @@
+
+ + +
@@ -136,7 +144,11 @@ let playerId = ""; // 当前房间id let roomId = ""; - const backendAddr = "192.168.31.222:8080" + // 当前是否是绘画者 + let isDrawer = false; + // 当前题目 + let currentTopic = ""; + const backendAddr = window.location.host // const backendAddr = "draw.airnan.cn" const api = "http://" + backendAddr + "/api" // const api = "https://" + backendAddr + "/api" @@ -390,6 +402,46 @@ if (result.type === "room-draw-clear") { clearDraw(); } + // 游戏开始 + if (result.type === "game-start") { + handleGameStart(result.data); + } + // 游戏结束 + if (result.type === "game-end") { + handleGameEnd(result.data); + } + // 轮次切换 + if (result.type === "round-change") { + handleRoundChange(result.data); + } + // 题目分配 + if (result.type === "topic-assign") { + handleTopicAssign(result.data); + } + // 答题正确 + if (result.type === "answer-correct") { + handleAnswerCorrect(result.data); + } + // 答题错误 + if (result.type === "answer-wrong") { + handleAnswerWrong(result.data); + } + // 分数更新 + if (result.type === "score-update") { + handleScoreUpdate(result.data); + } + // 倒计时 + if (result.type === "countdown") { + handleCountdown(result.data); + } + // 绘画者切换 + if (result.type === "drawer-change") { + handleDrawerChange(result.data); + } + // 绘画权限控制 + if (result.type === "draw-permission") { + handleDrawPermission(result.data); + } }; ws.onclose = function () { @@ -459,7 +511,11 @@ function sendInput() { let txt = $("#chat-input").val().trim(); if (txt !== "") { - sendMessage("room-chat", playerId + ":" + txt); + if (currentTopic && !isDrawer) { + sendMessage("room-chat-answer", txt); + } else { + sendMessage("room-chat", playerId + ":" + txt); + } $("#chat-input").val(""); } } @@ -490,6 +546,125 @@ $("#page-box").css("left", "0px"); } + function startGame() { + $.ajax({ + url: api + "/game/start", + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({roomId: roomId, playerId: playerId}), + success: function (data) { + if (data.success) { + showMsg(data.message); + } else { + showMsg(data.message); + } + }, + error: function (xhr, status, error) { + console.error('请求失败:', status, error); + } + }); + } + + function endGame() { + $.ajax({ + url: api + "/game/end", + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({roomId: roomId, playerId: playerId}), + success: function (data) { + if (data.success) { + showMsg(data.message); + } else { + showMsg(data.message); + } + }, + error: function (xhr, status, error) { + console.error('请求失败:', status, error); + } + }); + } + + function handleGameStart(data) { + showMsg("游戏开始!"); + $("#total-rounds").text(data.totalRounds); + $("#start-game-btn").hide(); + } + + function handleGameEnd(data) { + showMsg("游戏结束!"); + let winner = data.winner; + let finalScores = data.finalScores; + let scoreText = "最终得分:"; + for (let playerName in finalScores) { + scoreText += playerName + ":" + finalScores[playerName] + "分 "; + } + showMsg(scoreText); + $("#start-game-btn").show(); + $("#topic-name").text("等待游戏开始"); + $("#topic-tip").text("--"); + } + + function handleRoundChange(data) { + $("#current-round").text(data.currentRound); + showMsg("第 " + data.currentRound + " 轮开始"); + } + + function handleTopicAssign(data) { + let topic = data.topic; + currentTopic = topic.name; + if (isDrawer) { + $("#topic-name").text(topic.name); + $("#topic-tip").text(topic.tip); + } else { + $("#topic-name").text("????"); + $("#topic-tip").text(topic.tip); + } + } + + function handleAnswerCorrect(data) { + showMsg(data.playerName + " 猜对了!+" + data.score + "分"); + } + + function handleAnswerWrong(data) { + showMsg(data.playerName + " 猜错了"); + } + + function handleScoreUpdate(data) { + let scores = data.scores; + for (let i = 0; i < 5; i++) { + let playerItem = $("#player-" + i); + if (playerItem.length > 0) { + let playerName = playerItem.text(); + if (scores[playerName]) { + playerItem.text(playerName + ":" + scores[playerName] + "分"); + } + } + } + } + + function handleCountdown(data) { + $("#countdown").text(data.timeLeft); + } + + function handleDrawerChange(data) { + isDrawer = (data.drawerId === playerId); + showMsg(data.drawerName + " 开始绘画"); + if (isDrawer) { + showMsg("你是绘画者,请开始绘画!"); + } else { + showMsg("请猜答案!"); + } + } + + function handleDrawPermission(data) { + isDrawer = (data.drawerId === playerId); + if (isDrawer) { + $("#board").css("pointer-events", "auto"); + } else { + $("#board").css("pointer-events", "none"); + } + } +