From 673a277ea98db11f107f8e7c927d2d408525b9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E7=9A=AE=E5=B9=BC=E9=B8=9F?= <2960474346@qq.com> Date: Fri, 14 Jun 2024 18:51:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E4=BA=BA=E5=AF=B9=E6=88=98ws=E6=A1=86?= =?UTF-8?q?=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/WebSocketConfig.java | 5 + .../catchTheLetters/entity/GameMessage.java | 29 +++ .../catchTheLetters/entity/GameRoom.java | 52 +++-- .../catchTheLetters/entity/PlayerInGame.java | 29 +++ .../catchTheLetters/entity/PlayerInput.java | 18 ++ .../catchTheLetters/enums/InputKeyType.java | 21 ++ .../catchTheLetters/enums/InputState.java | 20 ++ .../catchTheLetters/enums/MessageType.java | 20 ++ .../catchTheLetters/enums/RoomStatus.java | 17 ++ .../example/catchTheLetters/enums/Status.java | 5 - .../handler/WebSocketHandler.java | 5 + .../catchTheLetters/model/vo/Letter.java | 22 ++- .../catchTheLetters/service/RoomService.java | 63 +++++- .../service/impl/RoomServiceImpl.java | 183 ++++++++++++++++++ .../com/example/catchTheLetters/JSONTest.java | 20 ++ 15 files changed, 465 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/example/catchTheLetters/entity/GameMessage.java create mode 100644 src/main/java/com/example/catchTheLetters/entity/PlayerInGame.java create mode 100644 src/main/java/com/example/catchTheLetters/entity/PlayerInput.java create mode 100644 src/main/java/com/example/catchTheLetters/enums/InputKeyType.java create mode 100644 src/main/java/com/example/catchTheLetters/enums/InputState.java create mode 100644 src/main/java/com/example/catchTheLetters/enums/MessageType.java create mode 100644 src/main/java/com/example/catchTheLetters/enums/RoomStatus.java delete mode 100644 src/main/java/com/example/catchTheLetters/enums/Status.java create mode 100644 src/main/java/com/example/catchTheLetters/service/impl/RoomServiceImpl.java create mode 100644 src/test/java/com/example/catchTheLetters/JSONTest.java diff --git a/src/main/java/com/example/catchTheLetters/config/WebSocketConfig.java b/src/main/java/com/example/catchTheLetters/config/WebSocketConfig.java index fc898ae..572b00c 100644 --- a/src/main/java/com/example/catchTheLetters/config/WebSocketConfig.java +++ b/src/main/java/com/example/catchTheLetters/config/WebSocketConfig.java @@ -7,6 +7,11 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +/** + * WebSocket 配置 + * + * @author spyn + */ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { diff --git a/src/main/java/com/example/catchTheLetters/entity/GameMessage.java b/src/main/java/com/example/catchTheLetters/entity/GameMessage.java new file mode 100644 index 0000000..1245597 --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/entity/GameMessage.java @@ -0,0 +1,29 @@ +package com.example.catchTheLetters.entity; + +import com.example.catchTheLetters.enums.MessageType; +import lombok.Data; + +import java.io.Serializable; + +/** + * 游戏消息 + * + * @auther spyn + */ +@Data +public class GameMessage implements Serializable { + private String roomId; + private MessageType type; + private T data; + + public GameMessage(MessageType type, T data) { + this.type = type; + this.data = data; + } + + public GameMessage(String roomId, MessageType type, T data) { + this.roomId = roomId; + this.type = type; + this.data = data; + } +} diff --git a/src/main/java/com/example/catchTheLetters/entity/GameRoom.java b/src/main/java/com/example/catchTheLetters/entity/GameRoom.java index 57bd85c..8819ef4 100644 --- a/src/main/java/com/example/catchTheLetters/entity/GameRoom.java +++ b/src/main/java/com/example/catchTheLetters/entity/GameRoom.java @@ -1,38 +1,32 @@ package com.example.catchTheLetters.entity; -import com.example.catchTheLetters.enums.Status; -import com.example.catchTheLetters.model.vo.Letter; +import cn.hutool.core.collection.ConcurrentHashSet; +import com.example.catchTheLetters.enums.RoomStatus; +import lombok.Data; import org.springframework.web.socket.WebSocketSession; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +/** + * 游戏房间 + * + * @author spyn + */ +@Data public class GameRoom { - private List players = new ArrayList<>(); - private Status status = Status.WAITING; + private final Map players = new ConcurrentHashMap<>(); + private final Set readyPlayers = new ConcurrentHashSet<>(); + private RoomStatus status = RoomStatus.WAITING; + private long roomId; + private WebSocketSession host; + // 单词 : 目前在拼这个单词的玩家数 + private final Map words = new ConcurrentSkipListMap<>(); - public void addPlayer(WebSocketSession session) { - players.add(session); - } - - public void removePlayer(WebSocketSession session) { - players.remove(session); - } - - public void startGame() { - status = Status.PLAYING; - } - - public void endGame() { - status = Status.END; - } - - public void handleInput(WebSocketSession session, String input) { - // 处理玩家输入 - } - - public Letter generateLetter() { - // 生成字母 - return null; + public GameRoom(WebSocketSession host, PlayerInGame player) { + this.roomId = UUID.randomUUID().getLeastSignificantBits(); + this.host = host; + players.put(host, player); } } diff --git a/src/main/java/com/example/catchTheLetters/entity/PlayerInGame.java b/src/main/java/com/example/catchTheLetters/entity/PlayerInGame.java new file mode 100644 index 0000000..ee6e3ce --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/entity/PlayerInGame.java @@ -0,0 +1,29 @@ +package com.example.catchTheLetters.entity; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 游戏中的玩家 + * + * @auther spyn + */ +@Data +public class PlayerInGame implements Serializable { + private String userId; + private User user; + private int score; + private int health; + private String currentWord; + private String currentAnswer; + + public PlayerInGame(String userId, User user) { + this.userId = userId; + this.user = user; + this.score = 0; + this.health = 100; + this.currentWord = ""; + this.currentAnswer = ""; + } +} diff --git a/src/main/java/com/example/catchTheLetters/entity/PlayerInput.java b/src/main/java/com/example/catchTheLetters/entity/PlayerInput.java new file mode 100644 index 0000000..93281eb --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/entity/PlayerInput.java @@ -0,0 +1,18 @@ +package com.example.catchTheLetters.entity; + +import com.example.catchTheLetters.enums.InputKeyType; +import com.example.catchTheLetters.enums.InputState; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class PlayerInput implements Serializable { + public InputKeyType key; + public InputState state; + + public PlayerInput(InputKeyType key, InputState state) { + this.key = key; + this.state = state; + } +} diff --git a/src/main/java/com/example/catchTheLetters/enums/InputKeyType.java b/src/main/java/com/example/catchTheLetters/enums/InputKeyType.java new file mode 100644 index 0000000..8f58707 --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/enums/InputKeyType.java @@ -0,0 +1,21 @@ +package com.example.catchTheLetters.enums; + +import lombok.Getter; + +/** + * 输入类型 + * + * @author spyn + */ +@Getter +public enum InputKeyType { + LEFT("left"), + RIGHT("right"), + SHIFT("shift"); + + private final String key; + + InputKeyType(String key) { + this.key = key; + } +} diff --git a/src/main/java/com/example/catchTheLetters/enums/InputState.java b/src/main/java/com/example/catchTheLetters/enums/InputState.java new file mode 100644 index 0000000..a984f00 --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/enums/InputState.java @@ -0,0 +1,20 @@ +package com.example.catchTheLetters.enums; + +import lombok.Getter; + +/** + * 输入状态 + * + * @author spyn + */ +@Getter +public enum InputState { + PRESS("press"), + RELEASE("release"); + + private final String state; + + InputState(String state) { + this.state = state; + } +} diff --git a/src/main/java/com/example/catchTheLetters/enums/MessageType.java b/src/main/java/com/example/catchTheLetters/enums/MessageType.java new file mode 100644 index 0000000..53ebbaa --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/enums/MessageType.java @@ -0,0 +1,20 @@ +package com.example.catchTheLetters.enums; + +/** + * WebSocket 消息类型 + * + * @auther spyn + */ +public enum MessageType { + INPUT("input"), + ROOM("room"), + LETTER("letter"), + SCORE("score"), + HEALTH("health"); + + private final String type; + + MessageType(String type) { + this.type = type; + } +} diff --git a/src/main/java/com/example/catchTheLetters/enums/RoomStatus.java b/src/main/java/com/example/catchTheLetters/enums/RoomStatus.java new file mode 100644 index 0000000..291f5bc --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/enums/RoomStatus.java @@ -0,0 +1,17 @@ +package com.example.catchTheLetters.enums; + +/** + * 房间状态 + * + * @author spyn + */ +public enum RoomStatus { + WAITING("waiting"), + PLAYING("playing"); + + private final String status; + + RoomStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/catchTheLetters/enums/Status.java b/src/main/java/com/example/catchTheLetters/enums/Status.java deleted file mode 100644 index 58dccdc..0000000 --- a/src/main/java/com/example/catchTheLetters/enums/Status.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.catchTheLetters.enums; - -public enum Status { - WAITING, PLAYING, END -} diff --git a/src/main/java/com/example/catchTheLetters/handler/WebSocketHandler.java b/src/main/java/com/example/catchTheLetters/handler/WebSocketHandler.java index e1302d7..c43c696 100644 --- a/src/main/java/com/example/catchTheLetters/handler/WebSocketHandler.java +++ b/src/main/java/com/example/catchTheLetters/handler/WebSocketHandler.java @@ -5,6 +5,11 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +/** + * WebSocket 处理器 + * + * @author spyn + */ @Controller public class WebSocketHandler extends TextWebSocketHandler { diff --git a/src/main/java/com/example/catchTheLetters/model/vo/Letter.java b/src/main/java/com/example/catchTheLetters/model/vo/Letter.java index d44f59c..14c45eb 100644 --- a/src/main/java/com/example/catchTheLetters/model/vo/Letter.java +++ b/src/main/java/com/example/catchTheLetters/model/vo/Letter.java @@ -1,5 +1,13 @@ package com.example.catchTheLetters.model.vo; +import lombok.Data; + +/** + * 字母 + * + * @author spyn + */ +@Data public class Letter { // 字母值,如果是10则是加血 private String letterVal; @@ -7,9 +15,17 @@ public class Letter { // 下落速度 private float speed = 3f; - // 字母的x坐标 + // 字母的x坐标,0-1之间 private float x; - // 字母的y坐标 - private float y; + public Letter(String letterVal, float x) { + this.letterVal = letterVal; + this.x = x; + } + + public Letter(String letterVal, float x, float speed) { + this.letterVal = letterVal; + this.x = x; + this.speed = speed; + } } diff --git a/src/main/java/com/example/catchTheLetters/service/RoomService.java b/src/main/java/com/example/catchTheLetters/service/RoomService.java index d494de4..6bc4053 100644 --- a/src/main/java/com/example/catchTheLetters/service/RoomService.java +++ b/src/main/java/com/example/catchTheLetters/service/RoomService.java @@ -1,18 +1,67 @@ package com.example.catchTheLetters.service; -import com.example.catchTheLetters.model.vo.Letter; +import com.example.catchTheLetters.entity.PlayerInput; import org.springframework.web.socket.WebSocketSession; +/** + * 房间服务 + * + * @author spyn + */ public interface RoomService { - void addPlayer(WebSocketSession session); + /** + * 添加玩家 + * @param roomId 房间号 + * @param session WebSocket 会话 + * @param token 玩家 token + */ + void addPlayer(long roomId, WebSocketSession session, String token); - void removePlayer(WebSocketSession session); + /** + * 移除玩家 + * @param roomId 房间号 + * @param session WebSocket 会话 + */ + void removePlayer(long roomId, WebSocketSession session); - void startGame(); + /** + * 开始游戏 + * @param roomId 房间号 + * @param session WebSocket 会话 + */ + void startGame(long roomId, WebSocketSession session); - void endGame(); + /** + * 取消开始游戏 + * @param roomId 房间号 + * @param session WebSocket 会话 + */ + void cancelStartGame(long roomId, WebSocketSession session); - void handleInput(WebSocketSession session, String input); + /** + * 结束游戏 + * @param roomId 房间号 + */ + void endGame(long roomId); - Letter generateLetter(); + /** + * 处理玩家输入 + * @param roomId 房间号 + * @param session WebSocket 会话 + * @param input 玩家输入 + */ + void handleInput(long roomId, WebSocketSession session, PlayerInput input); + + /** + * 创建房间 + * @param session WebSocket 会话 + * @param token 玩家 token + */ + void createRoom(WebSocketSession session, String token); + + /** + * 移除房间 + * @param roomId 房间号 + */ + void removeRoom(long roomId); } diff --git a/src/main/java/com/example/catchTheLetters/service/impl/RoomServiceImpl.java b/src/main/java/com/example/catchTheLetters/service/impl/RoomServiceImpl.java new file mode 100644 index 0000000..8e78f1e --- /dev/null +++ b/src/main/java/com/example/catchTheLetters/service/impl/RoomServiceImpl.java @@ -0,0 +1,183 @@ +package com.example.catchTheLetters.service.impl; + +import com.example.catchTheLetters.entity.*; +import com.example.catchTheLetters.enums.MessageType; +import com.example.catchTheLetters.enums.RoomStatus; +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.concurrent.ConcurrentHashMap; + +@Service +public class RoomServiceImpl implements RoomService { + + // 房间列表 + private final ConcurrentHashMap rooms = 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); + + // TODO 发送消息通知其他玩家有人加入了 + } + + @Override + public void removePlayer(long roomId, WebSocketSession session) { + var room = rooms.get(roomId); + var players = room.getPlayers(); + players.remove(session); + // 如果是房主退出,更换房主,否则关闭房间 + if (session == room.getHost()) { + if (!players.isEmpty()) { + room.setHost(players.keySet().iterator().next()); + } else { + rooms.remove(roomId); + } + } + // TODO 发送消息通知其他玩家有人退出了 + } + + @Override + public void startGame(long roomId, WebSocketSession session) { + var room = rooms.get(roomId); + var players = room.getPlayers(); + var readyPlayers = room.getReadyPlayers(); + readyPlayers.add(session); + // TODO 发送消息通知其他玩家有人准备好了 + // 如果所有玩家都准备好了,开始游戏 + if (readyPlayers.size() == room.getPlayers().size()) { + // TODO 发送消息通知所有玩家游戏开始 + 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); + // TODO 发送消息通知其他玩家有人取消准备 + } + + @Override + public void endGame(long roomId) { + var room = rooms.get(roomId); + room.setStatus(RoomStatus.WAITING); + // TODO 发送消息通知所有玩家游戏结束 + } + + @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(); + 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); + } + + @Override + public void removeRoom(long roomId) { + // 如果房间内还有玩家,全部移除 + var room = rooms.get(roomId); + var players = room.getPlayers(); + for (var player : players.keySet()) { + // TODO 发送消息通知所有玩家房间已解散 + } + rooms.remove(roomId); + } + + private void generateLetter(GameRoom room) { + var players = room.getPlayers(); + var words = room.getWords(); + + // 如果words长度<=5,从数据库中再获取一批随机单词 + if (words.size() <= 5) getWords(words); + + Letter letter; + + var val = random.nextInt(100); + + // 在80%概率当前单词的字母中随机选择一个,19%随机生成一个字母,1%是回血爱心。选定后随机赋值2f到6f的下落速度 + if (val == 0) + // 回血爱心 + letter = new Letter("10", random.nextFloat(1), random.nextFloat(2, 6)); + if (val >= 1 && val <= 81) + // 从当前单词中随机选择一个字母 + letter = new Letter(words.keySet().toArray()[random.nextInt(words.size())].toString(), random.nextFloat(1), random.nextFloat(2, 6)); + else + // 随机生成一个字母 + letter = new Letter(String.valueOf((char) (random.nextInt(26) + 'a')), random.nextFloat(1), random.nextFloat(2, 6)); + + // 给所有玩家发送字母 + for (var player : players.keySet()) + sendMessage(player, new GameMessage<>(MessageType.LETTER, letter)); + } + + private 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); + } + } + + private void getWords(Map words) { + // TODO 从数据库中获取一批随机单词,然后放入words中,并把单词数组推送给所有玩家 + } + + private void gameLogic(GameRoom room) { + getWords(room.getWords()); + // TODO 从300秒开始倒计时,每秒调用一次generateLetter方法,如果时间到了,调用endGame方法,如果当前单词被某玩家拼完,Map对应单词的value++,如果value>=玩家数(有可能中途有人退出),目前单词出队列,继续下一个单词 + } +} diff --git a/src/test/java/com/example/catchTheLetters/JSONTest.java b/src/test/java/com/example/catchTheLetters/JSONTest.java new file mode 100644 index 0000000..04b7010 --- /dev/null +++ b/src/test/java/com/example/catchTheLetters/JSONTest.java @@ -0,0 +1,20 @@ +package com.example.catchTheLetters; + +import com.example.catchTheLetters.entity.PlayerInput; +import com.example.catchTheLetters.enums.InputKeyType; +import com.example.catchTheLetters.enums.InputState; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class JSONTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void contextLoads() throws JsonProcessingException { + var input = new PlayerInput(InputKeyType.LEFT, InputState.PRESS); + System.out.println(objectMapper.writeValueAsString(input)); + } +}