🌟 两天时间我开发了一个卧底小游戏
前言 🌟
由于我最近开发了一个摸鱼小网站,我也琢磨着怎么让其更有趣,我灵机一动!🔥不如开发一个谁是卧底小游戏吧,大家还可以一起快乐玩耍,于是经过一番捣鼓我开发出了新功能===》谁是卧底(我才是卧底)。

开源 🚀
我的项目已经 500+ star 了,感谢大家的支持,还希望大家继续点赞~
在线体验地址 🔗:http://yucoder.cn/
后端地址 🌈:https://github.com/lhccong/fish-island-backend
前端地址 🏖️:https://github.com/lhccong/fish-island-frontend
思路 💡
1、谁是卧底肯定得有平民和卧底牌啊。
💡:AI 帮我生成了 200 个语义相近的词语。

2、规则呢怎么制定?

👦:我定了以下几条规则大家看看怎么样。
- 房间分为上帝模式和随机模式(上帝房主设置词语,随机就获取后台的系统内置词)。
- 游戏模式分为两种:常规模式(所有人都有词语,不知道身份)猜词模式(卧底知道身份但没有词语)。
- 在卧底聊天室中我还内置了摸鱼小助手,他会自动 Q 每一个流程。
- 每个人完成投票会自动结算(继续/结束,卧底淘汰还是平民淘汰)。
3、怎么打通玩家的交互实时更新呢?
👦:我的思路是这样的,每次有玩家进入游戏/投票的时候,后台推一条消息到前端,前端接收到消息就刷新数据。
4、前端怎么设计呢?
- 我直接放到我的摸鱼聊天室中(肯定是为了方便而且这样做相比其他的更加简单的实现)。

- 前端创建房间的时候可以选择模式。

-
支持同时查看房间列表(还没实现,暂时只支持一个房间)。
-
前端要支持小窗口聊天,还要支持查看历史消息,方便回溯。
-
前端窗口还要支持拖动。
代码实现 🔥
代码是容易写的,主要还是思路,下面是一些我的后台实现部分代码,供大家参考思路,着急写也配合了 AI 有错误大家可以指正下:
创建房间
▼java复制代码public String createRoom(UndercoverRoomCreateRequest request) { // 验证请求参数 if (request == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); } // 如果平民词语或卧底词语为空,从文件中随机读取一对词语 if (StringUtils.isBlank(request.getCivilianWord()) || StringUtils.isBlank(request.getUndercoverWord())) { try { // 从文件中读取词语对 String[] wordPair = getRandomWordPair(); if (wordPair != null && wordPair.length == 2) { request.setCivilianWord(wordPair[0]); request.setUndercoverWord(wordPair[1]); } else { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取词语对失败"); } } catch (IOException e) { log.error("读取词语文件失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "读取词语文件失败"); } } // 再次验证词语是否为空 if (StringUtils.isBlank(request.getCivilianWord())) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "平民词语不能为空"); } if (StringUtils.isBlank(request.getUndercoverWord())) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "卧底词语不能为空"); } if (request.getDuration() == null || request.getDuration() < 60) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "游戏持续时间不能少于60秒"); } // 验证最大人数 if (request.getMaxPlayers() == null || request.getMaxPlayers() < 3) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "房间最大人数不能少于3人"); } if (request.getMaxPlayers() > 20) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "房间最大人数不能超过20人"); } // 验证是否为管理员 User loginUser = userService.getLoginUser(); // 使用分布式锁确保同一时间只能有一个房间 RLock lock = redissonClient.getLock("undercover_room_create_lock"); try { boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS); if (!isLocked) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作频繁,请稍后再试"); } // 检查是否已有活跃房间 String activeRoomId = stringRedisTemplate.opsForValue().get(UndercoverGameRedisKey.ACTIVE_ROOM); if (activeRoomId != null) { // 获取房间信息 String roomJson = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, activeRoomId)); if (roomJson != null) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "已存在活跃房间,请等待当前房间结束"); } } // 创建新房间 UndercoverRoom room = new UndercoverRoom(); room.setStatus(RoomStatusEnum.WAITING); room.setParticipantIds(new HashSet<>()); room.setUndercoverIds(new HashSet<>()); room.setCivilianIds(new HashSet<>()); room.setEliminatedIds(new HashSet<>()); room.setCivilianWord(request.getCivilianWord()); room.setUndercoverWord(request.getUndercoverWord()); room.setCreateTime(new Date()); room.setDuration(request.getDuration()); room.setCreatorId(loginUser.getId()); room.setMaxPlayers(request.getMaxPlayers()); // 设置游戏模式,默认为常规模式 room.setGameMode(request.getGameMode() != null ? request.getGameMode() : 1); // 生成房间ID String roomId = UUID.randomUUID().toString().replace("-", ""); // 存储房间信息到Redis try { String roomJson = objectMapper.writeValueAsString(room); stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, roomId), roomJson, 60, TimeUnit.MINUTES ); // 设置当前活跃房间 stringRedisTemplate.opsForValue().set(UndercoverGameRedisKey.ACTIVE_ROOM, roomId, 60, TimeUnit.MINUTES); MessageWrapper messageWrapper = getSystemMessageWrapper(loginUser.getUserName()+"创建了一个紧张刺激的谁是卧底房间,大家快来参加吧~"); webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.CHAT.getType()) .data(messageWrapper).build()); webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.REFRESH_ROOM.getType()) .data("").build()); return roomId; } catch (JsonProcessingException e) { log.error("序列化房间信息失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "创建房间失败"); } } catch (InterruptedException e) { log.error("获取锁失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后再试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
判定游戏结束
▼java复制代码public boolean endGame(String roomId) { //暂时无需 ID 自动获取 roomId = stringRedisTemplate.opsForValue().get(UndercoverGameRedisKey.ACTIVE_ROOM); // 验证参数 if (StringUtils.isBlank(roomId)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "房间ID不能为空"); } // 使用分布式锁确保并发安全 RLock lock = redissonClient.getLock("undercover_room_end_lock:" + roomId); try { boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS); if (!isLocked) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作频繁,请稍后再试"); } // 获取房间信息 String roomJson = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, roomId)); if (roomJson == null) { throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "房间不存在"); } try { UndercoverRoom room = objectMapper.readValue(roomJson, UndercoverRoom.class); // 如果游戏未开始,直接结束 if (room.getStatus() != RoomStatusEnum.PLAYING) { // 更新房间状态 room.setStatus(RoomStatusEnum.ENDED); // 更新房间信息 String updatedRoomJson = objectMapper.writeValueAsString(room); stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, roomId), updatedRoomJson, 60, TimeUnit.MINUTES ); // 清除活跃房间 String activeRoomId = stringRedisTemplate.opsForValue().get(UndercoverGameRedisKey.ACTIVE_ROOM); if (roomId.equals(activeRoomId)) { stringRedisTemplate.delete(UndercoverGameRedisKey.ACTIVE_ROOM); } return true; } // 如果游戏已开始,进行投票统计和游戏结果判断 // 1. 统计投票数,找出票数最多的玩家 Map<Long, Integer> voteCountMap = new HashMap<>(); Long mostVotedPlayer = null; int maxVotes = -1; // 获取所有未淘汰的玩家 Set<Long> activePlayers = new HashSet<>(room.getParticipantIds()); activePlayers.removeAll(room.getEliminatedIds()); // 统计每个玩家的票数 for (Long playerId : activePlayers) { String voteCountStr = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTE_COUNT, roomId) + ":" + playerId); int voteCount = 0; if (voteCountStr != null) { voteCount = Integer.parseInt(voteCountStr); } voteCountMap.put(playerId, voteCount); // 更新最高票数玩家 if (voteCount > maxVotes) { maxVotes = voteCount; mostVotedPlayer = playerId; } } // 2. 如果有投票,处理投票结果 boolean shouldEndGame = false; String gameResult = ""; if (maxVotes == 0) { shouldEndGame = true; gameResult = "暂无人投票,游戏结束参与积分已退回"; } if (mostVotedPlayer != null && maxVotes > 0) { // 判断最高票数的玩家是否为卧底 boolean isUndercover = room.getUndercoverIds().contains(mostVotedPlayer); // 获取被淘汰玩家信息 User eliminatedUser = userService.getById(mostVotedPlayer); String eliminatedUserName = eliminatedUser != null ? eliminatedUser.getUserName() : "未知玩家"; // 淘汰投票最多的玩家 room.getEliminatedIds().add(mostVotedPlayer); // 计算剩余卧底和平民数量 int remainingUndercovers = 0; int remainingCivilians = 0; for (Long userId : room.getUndercoverIds()) { if (!room.getEliminatedIds().contains(userId)) { remainingUndercovers++; } } for (Long userId : room.getCivilianIds()) { if (!room.getEliminatedIds().contains(userId)) { remainingCivilians++; } } // 判断游戏是否结束 if (remainingUndercovers == 0) { // 所有卧底被淘汰,平民获胜 shouldEndGame = true; // 获取所有卧底的名字 StringBuilder undercoverNames = new StringBuilder(); for (Long undercoverId : room.getUndercoverIds()) { User undercoverUser = userService.getById(undercoverId); if (undercoverUser != null) { if (undercoverNames.length() > 0) { undercoverNames.append("、"); } undercoverNames.append(undercoverUser.getUserName()); } } gameResult = "平民获胜!所有卧底已被淘汰!卧底是:" + undercoverNames + "。平民词语是【" + room.getCivilianWord() + "】,卧底词语是【" + room.getUndercoverWord() + "】"; } else if (remainingUndercovers >= remainingCivilians) { // 卧底人数大于等于平民人数,卧底获胜 shouldEndGame = true; // 获取所有卧底的名字 StringBuilder undercoverNames = new StringBuilder(); for (Long undercoverId : room.getUndercoverIds()) { User undercoverUser = userService.getById(undercoverId); if (undercoverUser != null) { if (undercoverNames.length() > 0) { undercoverNames.append("、"); } undercoverNames.append(undercoverUser.getUserName()); } } gameResult = "卧底获胜!卧底人数已超过或等于平民人数!卧底是:" + undercoverNames.toString() + "。平民词语是【" + room.getCivilianWord() + "】,卧底词语是【" + room.getUndercoverWord() + "】"; } else { // 游戏继续,显示谁被淘汰了 if (isUndercover) { gameResult = "玩家【" + eliminatedUserName + "】被淘汰,他是卧底!。还有" + remainingUndercovers + "名卧底未被发现,游戏继续..."; } else { gameResult = "玩家【" + eliminatedUserName + "】被淘汰,他是平民!剩余平民" + remainingCivilians + "人,卧底" + remainingUndercovers + "人,游戏继续..."; } // 保存淘汰信息但不结束游戏 stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_RESULT, roomId), gameResult, 60, TimeUnit.MINUTES ); } } // 3. 更新游戏状态 if (shouldEndGame) { room.setStatus(RoomStatusEnum.ENDED); // 将游戏结果保存到 Redis,可以添加一个新的键 stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_RESULT, roomId), gameResult, 60, TimeUnit.MINUTES ); // 清除活跃房间 String activeRoomId = stringRedisTemplate.opsForValue().get(UndercoverGameRedisKey.ACTIVE_ROOM); if (roomId.equals(activeRoomId)) { stringRedisTemplate.delete(UndercoverGameRedisKey.ACTIVE_ROOM); } // 清除所有玩家的角色信息 for (Long playerId : room.getParticipantIds()) { // 删除玩家角色信息 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_ROLE, playerId) ); // 删除玩家所在房间信息 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_ROOM, playerId) ); // 删除玩家的投票状态 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_VOTED, roomId, playerId) ); // 删除玩家收到的投票计数 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTE_COUNT, roomId) + ":" + playerId ); // 删除玩家猜词次数记录 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_GUESS_COUNT, roomId, playerId) ); } // 删除房间的投票记录 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTES, roomId) ); } else { // 如果游戏继续,确保状态为 PLAYING room.setStatus(RoomStatusEnum.PLAYING); // 如果有投票记录,需要清除所有玩家的投票状态,以便下一轮投票 if (mostVotedPlayer != null) { // 清除所有玩家的投票状态 for (Long playerId : room.getParticipantIds()) { stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_VOTED, roomId, playerId) ); } // 清除投票计数 for (Long playerId : room.getParticipantIds()) { stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTE_COUNT, roomId) + ":" + playerId ); } // 清除投票记录 stringRedisTemplate.delete( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTES, roomId) ); } } // 更新房间信息 String updatedRoomJson = objectMapper.writeValueAsString(room); stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, roomId), updatedRoomJson, 60, TimeUnit.MINUTES ); //发送消息给每个人 MessageWrapper messageWrapper = getSystemMessageWrapper(gameResult); webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.UNDERCOVER.getType()) .data(messageWrapper).build()); return true; } catch (JsonProcessingException e) { log.error("解析房间信息失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "结束游戏失败"); } } catch (InterruptedException e) { log.error("获取锁失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后再试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
给用户投票
▼java复制代码public boolean vote(UndercoverVoteRequest request) { // 验证参数 if (request == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "投票请求不能为空"); } String roomId; //暂时无需 ID 自动获取 roomId = stringRedisTemplate.opsForValue().get(UndercoverGameRedisKey.ACTIVE_ROOM); if (StringUtils.isBlank(roomId)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "房间ID不能为空"); } Long targetId = request.getTargetId(); if (targetId == null || targetId <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "投票目标不合法"); } // 验证用户登录状态 User loginUser = userService.getLoginUser(); // 使用分布式锁确保并发安全 RLock lock = redissonClient.getLock("undercover_room_vote_lock:" + roomId); try { boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS); if (!isLocked) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作频繁,请稍后再试"); } // 获取房间信息 String roomJson = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_INFO, roomId)); if (roomJson == null) { throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "房间不存在"); } try { UndercoverRoom room = objectMapper.readValue(roomJson, UndercoverRoom.class); // 检查房间状态 if (room.getStatus() != RoomStatusEnum.PLAYING) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "房间未开始游戏或已结束"); } // 检查用户是否在房间中 if (!room.getParticipantIds().contains(loginUser.getId())) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户不在房间中"); } // 检查用户是否已被淘汰 if (room.getEliminatedIds().contains(loginUser.getId())) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户已被淘汰"); } // 检查投票目标是否在房间中 if (!room.getParticipantIds().contains(targetId)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "投票目标不在房间中"); } // 检查投票目标是否已被淘汰 if (room.getEliminatedIds().contains(targetId)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "投票目标已被淘汰"); } // 检查是否已投票 String hasVoted = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_VOTED, roomId, loginUser.getId())); if (hasVoted != null) { throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户已投票"); } // 获取投票者和目标用户信息 User voter = userService.getById(loginUser.getId()); User target = userService.getById(targetId); // 创建投票记录 UndercoverVoteVO voteVO = new UndercoverVoteVO(); voteVO.setVoterId(loginUser.getId()); voteVO.setVoterName(voter.getUserName()); voteVO.setVoterAvatar(voter.getUserAvatar()); voteVO.setTargetId(targetId); voteVO.setTargetName(target.getUserName()); voteVO.setTargetAvatar(target.getUserAvatar()); voteVO.setVoteTime(new Date()); // 获取当前投票记录 List<UndercoverVoteVO> votes = new ArrayList<>(); String votesJson = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTES, roomId)); if (votesJson != null) { votes = objectMapper.readValue(votesJson, new TypeReference<List<UndercoverVoteVO>>() { }); } // 添加新投票记录 votes.add(voteVO); // 更新投票记录 String updatedVotesJson = objectMapper.writeValueAsString(votes); stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTES, roomId), updatedVotesJson, 60, TimeUnit.MINUTES ); // 更新投票计数 String voteCountKey = UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.ROOM_VOTE_COUNT, roomId) + ":" + targetId; String voteCountStr = stringRedisTemplate.opsForValue().get(voteCountKey); int voteCount = 1; if (voteCountStr != null) { voteCount = Integer.parseInt(voteCountStr) + 1; } stringRedisTemplate.opsForValue().set(voteCountKey, String.valueOf(voteCount), 60, TimeUnit.MINUTES); // 标记用户已投票 stringRedisTemplate.opsForValue().set( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_VOTED, roomId, loginUser.getId()), "1", 60, TimeUnit.MINUTES ); MessageWrapper messageWrapper = getSystemMessageWrapper(loginUser.getUserName() + "用户已完成投票"); webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.UNDERCOVER.getType()) .data(messageWrapper).build()); // 检查是否所有未淘汰的玩家都已投票 boolean allVoted = true; // 获取所有未淘汰的玩家 Set<Long> activePlayers = new HashSet<>(room.getParticipantIds()); activePlayers.removeAll(room.getEliminatedIds()); // 检查每个未淘汰玩家是否都已投票 for (Long playerId : activePlayers) { String playerVoted = stringRedisTemplate.opsForValue().get( UndercoverGameRedisKey.getKey(UndercoverGameRedisKey.PLAYER_VOTED, roomId, playerId)); if (playerVoted == null) { allVoted = false; break; } } // 如果所有未淘汰玩家都已投票,自动触发投票结算 if (allVoted) { MessageWrapper allVotedMessage = getSystemMessageWrapper("所有玩家已完成投票,即将进行投票结算"); webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.UNDERCOVER.getType()) .data(allVotedMessage).build()); // 触发投票结算方法 endGame(roomId); } webSocketService.sendToAllOnline(WSBaseResp.builder() .type(MessageTypeEnum.REFRESH_ROOM.getType()) .data("").build()); return true; } catch (JsonProcessingException e) { log.error("处理投票失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "投票失败"); } } catch (InterruptedException e) { log.error("获取锁失败", e); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后再试"); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
最后 🌸
代码都是容易写的,主要还是思路和想法,我把自己的思路分享出来和大家一起,大家有啥思路欢迎一起讨论~
感谢大家看到最后,我是聪 希望可以跟大家一起学习,如果文章对大家有帮助的话不妨给我的新项目 摸鱼岛🐟 点个免费的 star 🌟,我的 Github:github.com/lhccong 如果里面有你感兴趣的项目也可以点个星星⭐和关注🔥,未来我还会持续写新的好玩的小项目。
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
