package com.example.catchTheLetters.service.impl; import com.example.catchTheLetters.entity.*; import com.example.catchTheLetters.enums.*; import com.example.catchTheLetters.model.vo.Letter; import com.example.catchTheLetters.service.AuthService; import com.example.catchTheLetters.service.RoomService; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.stereotype.Service; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * 房间服务实现类 * * @author spyn */ @Service public class RoomServiceImpl implements RoomService { // 房间列表 private final ConcurrentHashMap rooms = new ConcurrentHashMap<>(); // 玩家列表 private final ConcurrentHashMap playerInGame = new ConcurrentHashMap<>(); private final ObjectMapper objectMapper = new ObjectMapper(); private final Random random = new Random(); @Resource private MongoTemplate mongoTemplate; @Resource private AuthService authService; @Override public void addPlayer(long roomId, WebSocketSession session, String token) { var room = rooms.get(roomId); var players = room.getPlayers(); var user = authService.verify(token); var player = new PlayerInGame(user.getId(), user); // 如果玩家已经在房间中,更新他们的WebSocketSession for (var entry : players.entrySet()) { if (entry.getValue().getUserId().equals(user.getId())) { player = entry.getValue(); players.remove(entry.getKey()); break; } } players.put(session, player); // 发送消息通知其他玩家有人加入了 var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.JOIN, player.getUserId())); for (var playerEntry : players.entrySet()) if (playerEntry.getKey() != session) sendMessage(playerEntry.getKey(), message); } @Override public void removePlayer(long roomId, WebSocketSession session) { var room = rooms.get(roomId); var players = room.getPlayers(); var removed = players.remove(session); // 如果是房主退出,更换房主,否则关闭房间 if (session == room.getHost()) { if (!players.isEmpty()) room.setHost(players.keySet().iterator().next()); else rooms.remove(roomId); } // 发送消息通知其他玩家有人退出了 var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.LEAVE, removed.getUserId())); for (var player : players.keySet()) if (player != session) sendMessage(player, message); } @Override public void kickPlayer(long roomId, WebSocketSession session, String playerID) { var room = rooms.get(roomId); // 如果不是房主,返回错误信息 if (session != room.getHost()) { sendMessage(session, new GameMessage<>(MessageType.ERROR, "你不是房主")); return; } var players = room.getPlayers(); var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.KICK, playerID)); WebSocketSession aimSession = null; for (var player : players.entrySet()) { if (player.getValue().getUserId().equals(playerID)) aimSession = player.getKey(); // 发送消息通知有玩家被踢出了 sendMessage(player.getKey(), message); } players.remove(aimSession); } @Override public void startGame(long roomId, WebSocketSession session) { var room = rooms.get(roomId); var players = room.getPlayers(); var readyPlayers = room.getReadyPlayers(); readyPlayers.add(session); // 发送消息通知其他玩家有人准备好了 var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.START, session.getId())); for (var player : players.keySet()) if (player != session) sendMessage(player, message); // 如果所有玩家都准备好了,开始游戏 if (readyPlayers.size() == room.getPlayers().size()) { // 发送消息通知所有玩家游戏开始 var startMessage = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.START, session.getId())); for (var player : players.keySet()) sendMessage(player, startMessage); readyPlayers.clear(); room.setStatus(RoomStatus.PLAYING); gameLogic(room); } } @Override public void cancelStartGame(long roomId, WebSocketSession session) { var room = rooms.get(roomId); var readyPlayers = room.getReadyPlayers(); readyPlayers.remove(session); // 发送消息通知其他玩家有人取消准备 var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.CANCEL_START, session.getId())); for (var player : room.getPlayers().keySet()) if (player != session) sendMessage(player, message); } @Override public void handleInput(long roomId, WebSocketSession session, PlayerInput input) { var message = new GameMessage<>(MessageType.INPUT, input); var room = rooms.get(roomId); var players = room.getPlayers(); if (input.getKey() == InputKeyType.SPACE) { var player = players.get(session); // 如果是空格,按下则记录,松开则获取记录时间并判断是否有1秒,有则对玩家发送清空答案的消息,并清空答案 if (input.getState() == InputState.PRESS) player.setDropTime(System.currentTimeMillis()); else { var dropTime = player.getDropTime(); if (System.currentTimeMillis() - dropTime >= 1000) { player.clearCurrentAnswer(); sendMessage(session, new GameMessage<>(MessageType.LETTER, new LetterAction(LetterActionType.CLEAR))); } } } else { // 如果是移动,发送消息通知其他玩家有人输入了 for (var player : players.keySet()) if (player != session) sendMessage(player, message); } } @Override public void createRoom(WebSocketSession session, String token) { var user = authService.verify(token); var player = new PlayerInGame(user.getId(), user); var room = new GameRoom(session, player); rooms.put(room.getRoomId(), room); // 发送消息通知玩家创建房间成功 sendMessage(session, new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.CREATE, room.getRoomId()))); } @Override public void removeRoom(long roomId) { // 如果房间内还有玩家,全部移除 var room = rooms.get(roomId); var players = room.getPlayers(); for (var player : players.entrySet()) { // 发送消息通知所有玩家房间已解散 sendMessage(player.getKey(), new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.KICK, player.getValue().getUserId()))); } rooms.remove(roomId); } @Override public void invitePlayer(long roomId, WebSocketSession session, String playerID) { var roomAction = new RoomAction(); roomAction.setType(RoomActionType.INVITE); roomAction.setRoomID(roomId); roomAction.setUserID(playerID); var aimSession = playerInGame.get(playerID); // 如果玩家在线,发送邀请消息 if (aimSession != null) sendMessage(session, new GameMessage<>(MessageType.ROOM, roomAction)); else sendMessage(session, new GameMessage<>(MessageType.ERROR, "玩家不在线")); } @Override public void connect(WebSocketSession session, String token) { var player = authService.verify(token); playerInGame.put(player.getId(), session); } @Override public void disconnect(String token) { var player = authService.verify(token); var session = playerInGame.remove(player.getId()); try { session.close(); } catch (IOException e) { throw new RuntimeException("关闭WebSocket会话失败", e); } } @Override public void sendMessage(WebSocketSession session, T message) { String json; if (!(message instanceof String)) { try { json = objectMapper.writeValueAsString(message); } catch (Exception e) { throw new RuntimeException("消息转为JSON字符串操作失败", e); } } else json = (String) message; try { session.sendMessage(new TextMessage(json)); } catch (IOException e) { throw new RuntimeException("发送消息失败", e); } } @Override public void getLetter(long roomId, WebSocketSession session, long letterId) { var room = rooms.get(roomId); var letter = room.getLetters().get(letterId); // 如果没有这个字母,代表被其他玩家拿走了 if (letter == null) return; var players = room.getPlayers(); var player = players.get(session); // 发送消息通知其他玩家有人获得了字母 var message = new GameMessage<>(MessageType.LETTER, new LetterAction(LetterActionType.GET, letter, player.getUserId())); for (var playerEntry : players.entrySet()) sendMessage(playerEntry.getKey(), message); // TODO 字母校验和加分、改变血量逻辑 // 玩家接取字母时,向玩家当前答案中添加字母,如果单词拼完,给玩家加5*word.length的分和10滴血,如果单词拼错,每秒扣5滴血,如果玩家血量为0,则死亡 // 如果接取的是回血爱心,给玩家加10滴血 } private void generateLetter(GameRoom room) { var players = room.getPlayers(); var words = room.getWords(); // 如果words长度<=5,从数据库中再获取一批随机单词 if (words.size() <= 5) getWords(words); Letter letter; var id = UUID.randomUUID().getLeastSignificantBits(); var val = random.nextInt(100); // 在80%概率当前单词的字母中随机选择一个,19%随机生成一个字母,1%是回血爱心。选定后随机赋值2f到6f的下落速度 if (val == 0) // 回血爱心 letter = new Letter(id, "10", random.nextFloat(1), random.nextFloat(2, 6)); else if (val <= 81) // 从当前单词中随机选择一个字母 letter = new Letter(id, words.keySet().toArray()[random.nextInt(words.size())].toString(), random.nextFloat(1), random.nextFloat(2, 6)); else // 随机生成一个字母 letter = new Letter(id, String.valueOf((char) (random.nextInt(26) + 'a')), random.nextFloat(1), random.nextFloat(2, 6)); room.getLetters().put(id, letter); // 给所有玩家发送字母 for (var player : players.keySet()) sendMessage(player, new GameMessage<>(MessageType.LETTER, new LetterAction(LetterActionType.CREATE, letter))); } private void getWords(Map words) { // TODO 从数据库中获取一批随机单词,然后放入words中,并把单词数组推送给所有玩家 } private void gameLogic(GameRoom room) { getWords(room.getWords()); // TODO 从300秒开始倒计时,每秒调用一次generateLetter方法,如果时间到了,调用endGame方法 // 如果当前单词被某玩家拼完,Map对应单词的value++,如果value>=玩家数(有可能中途有人退出),目前单词出队列,继续下一个单词 // 游戏结束后需要向玩家发送排行榜数据(用户ID、分数、排名) // 最大的问题:丢包后如何处理?比如其他玩家按下按键后,自己没有接收到松开消息,导致其他玩家在画面中一直持续运动,网络连接稳定后,不同C端的玩家位置不一致 } public void endGame(long roomId) { var room = rooms.get(roomId); room.setStatus(RoomStatus.WAITING); // 发送消息通知所有玩家游戏结束 var message = new GameMessage<>(MessageType.ROOM, new RoomAction(RoomActionType.END)); for (var player : room.getPlayers().keySet()) sendMessage(player, message); // TODO 初始化玩家数据 } }