diff --git a/be/src/main/java/codesquad/gaemimarble/exception/CustomException.java b/be/src/main/java/codesquad/gaemimarble/exception/CustomException.java new file mode 100644 index 0000000..ed29430 --- /dev/null +++ b/be/src/main/java/codesquad/gaemimarble/exception/CustomException.java @@ -0,0 +1,15 @@ +package codesquad.gaemimarble.exception; + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException { + private final String playerId; + private final Long gameId; + + public CustomException(String message, String playerId, Long gameId) { + super(message); + this.playerId = playerId; + this.gameId = gameId; + } +} diff --git a/be/src/main/java/codesquad/gaemimarble/filter/WebSocketHandler.java b/be/src/main/java/codesquad/gaemimarble/filter/WebSocketHandler.java index 0e6021e..cb318ed 100644 --- a/be/src/main/java/codesquad/gaemimarble/filter/WebSocketHandler.java +++ b/be/src/main/java/codesquad/gaemimarble/filter/WebSocketHandler.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import codesquad.gaemimarble.exception.CustomException; +import codesquad.gaemimarble.game.controller.SocketDataSender; import codesquad.gaemimarble.game.dto.request.GameMessage; import codesquad.gaemimarble.game.controller.GameController; import lombok.RequiredArgsConstructor; @@ -18,6 +20,7 @@ public class WebSocketHandler extends TextWebSocketHandler { private final ObjectMapper objectMapper; private final GameController gameController; + private final SocketDataSender socketDataSender; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { @@ -37,7 +40,11 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message) payload, expectedClass); log.info("payload:{}", payload); log.info("className:{}", mappedRequest.getClass().cast(mappedRequest)); - gameController.handleRequest(mappedRequest); + try { + gameController.handleRequest(mappedRequest); + } catch (CustomException ex) { + socketDataSender.sendErrorMessage(ex.getGameId(), ex.getPlayerId(), ex.getMessage()); + } } private Long extractGameIdFromUri(String uri) { diff --git a/be/src/main/java/codesquad/gaemimarble/game/controller/GameController.java b/be/src/main/java/codesquad/gaemimarble/game/controller/GameController.java index eb6a430..cc43adf 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/controller/GameController.java +++ b/be/src/main/java/codesquad/gaemimarble/game/controller/GameController.java @@ -99,6 +99,12 @@ private void sendEventResult(GameEventResultRequest gameEventResultRequest) { gameEventNameResponse)); socketDataSender.send(gameEventResultRequest.getGameId(), new ResponseDTO<>(TypeConstants.STATUS_BOARD, gameService.proceedEvent(gameEventNameResponse.getName(), gameEventResultRequest.getGameId()))); + if (gameService.checkGameOver(gameEventResultRequest.getGameId())) { + socketDataSender.send( + gameEventResultRequest.getGameId(), new ResponseDTO<>(TypeConstants.GAME_OVER, + gameService.createUserRanking(gameEventResultRequest.getGameId()))); + socketDataSender.close(gameEventResultRequest.getGameId()); + } } @PostMapping("/api/games") diff --git a/be/src/main/java/codesquad/gaemimarble/game/controller/SocketDataSender.java b/be/src/main/java/codesquad/gaemimarble/game/controller/SocketDataSender.java index 1c80040..77c791a 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/controller/SocketDataSender.java +++ b/be/src/main/java/codesquad/gaemimarble/game/controller/SocketDataSender.java @@ -1,8 +1,6 @@ package codesquad.gaemimarble.game.controller; import java.io.IOException; -import java.util.HashSet; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -10,7 +8,6 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import codesquad.gaemimarble.game.dto.ResponseDTO; @@ -23,32 +20,31 @@ @RequiredArgsConstructor @Slf4j public class SocketDataSender { - private final ConcurrentMap> gameSocketMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> gameSocketMap = new ConcurrentHashMap<>(); private final ObjectMapper objectMapper; public void createRoom(Long gameRoomId) { - gameSocketMap.put(gameRoomId, new HashSet<>()); + gameSocketMap.put(gameRoomId, new ConcurrentHashMap<>()); } public boolean saveSocket(Long gameId, String playerId, WebSocketSession session) { - Set sessions = gameSocketMap.computeIfAbsent(gameId, key -> ConcurrentHashMap.newKeySet()); + ConcurrentMap socketMap = gameSocketMap.get(gameId); try { - if (sessions.size() == 4) { + if (socketMap.values().size() == 4) { session.sendMessage(new TextMessage(objectMapper.writeValueAsString( - new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse("full", "인원이 가득 찼습니다."))))); + new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse("인원이 가득 찼습니다."))))); + session.close(); return false; } - boolean isDuplicate = sessions.stream() - .anyMatch(s -> s.getAttributes().get("playerId").equals(playerId)); + boolean isDuplicate = socketMap.containsKey(playerId); if (!isDuplicate) { - session.getAttributes().put("playerId", playerId); - sessions.add(session); + socketMap.put(playerId, session); return true; } else { session.sendMessage(new TextMessage(objectMapper.writeValueAsString( - new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse("duplicate", "이미 접속한 플레이어입니다."))))); + new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse("이미 접속한 플레이어입니다."))))); return false; } } catch (IOException e) { @@ -58,7 +54,7 @@ public boolean saveSocket(Long gameId, String playerId, WebSocketSession session } public void send(Long gameId, T object) { - for (WebSocketSession session : gameSocketMap.get(gameId)) { + for (WebSocketSession session : gameSocketMap.get(gameId).values()) { try { session.sendMessage(new TextMessage(objectMapper.writeValueAsString(object))); } catch (IOException e) { @@ -67,4 +63,28 @@ public void send(Long gameId, T object) { } System.out.println("전송 완료"); } + + public void sendErrorMessage(Long gameId, String playerId, String message) { + try { + if (playerId == null) { + send(gameId, new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse(message))); + return; + } + gameSocketMap.get(gameId).get(playerId).sendMessage(new TextMessage(objectMapper.writeValueAsString( + new ResponseDTO<>(TypeConstants.ERROR, new SocketErrorResponse(message))))); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + public void close(Long gameId) { + for (WebSocketSession session : gameSocketMap.get(gameId).values()) { + try { + session.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + gameSocketMap.remove(gameId); + } } diff --git a/be/src/main/java/codesquad/gaemimarble/game/dto/SocketErrorResponse.java b/be/src/main/java/codesquad/gaemimarble/game/dto/SocketErrorResponse.java index bafcc83..cfddab9 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/dto/SocketErrorResponse.java +++ b/be/src/main/java/codesquad/gaemimarble/game/dto/SocketErrorResponse.java @@ -5,12 +5,10 @@ @Getter public class SocketErrorResponse { - private String errorType; // 나중에 에러 코드로 변경 private String message; @Builder - public SocketErrorResponse(String errorType, String message) { - this.errorType = errorType; + public SocketErrorResponse(String message) { this.message = message; } } diff --git a/be/src/main/java/codesquad/gaemimarble/game/dto/response/PlayerAsset.java b/be/src/main/java/codesquad/gaemimarble/game/dto/response/PlayerAsset.java new file mode 100644 index 0000000..772cfe8 --- /dev/null +++ b/be/src/main/java/codesquad/gaemimarble/game/dto/response/PlayerAsset.java @@ -0,0 +1,16 @@ +package codesquad.gaemimarble.game.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PlayerAsset { + private final String playerId; + private final Integer totalAsset; + + @Builder + private PlayerAsset(String playerId, Integer totalAsset) { + this.playerId = playerId; + this.totalAsset = totalAsset; + } +} diff --git a/be/src/main/java/codesquad/gaemimarble/game/dto/response/UserRankingResponse.java b/be/src/main/java/codesquad/gaemimarble/game/dto/response/UserRankingResponse.java new file mode 100644 index 0000000..fc5a725 --- /dev/null +++ b/be/src/main/java/codesquad/gaemimarble/game/dto/response/UserRankingResponse.java @@ -0,0 +1,16 @@ +package codesquad.gaemimarble.game.dto.response; + +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class UserRankingResponse { + List ranking; + + @Builder + private UserRankingResponse(List ranking) { + this.ranking = ranking; + } +} diff --git a/be/src/main/java/codesquad/gaemimarble/game/entity/GameStatus.java b/be/src/main/java/codesquad/gaemimarble/game/entity/GameStatus.java index 3851260..7ab4a37 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/entity/GameStatus.java +++ b/be/src/main/java/codesquad/gaemimarble/game/entity/GameStatus.java @@ -47,4 +47,8 @@ public Player getPlayer(String playerId) { .findFirst() .orElseThrow(() -> new IllegalArgumentException("해당하는 플레이어가 없습니다.")); } + + public void incrementRoundCount() { + roundCount++; + } } diff --git a/be/src/main/java/codesquad/gaemimarble/game/entity/TypeConstants.java b/be/src/main/java/codesquad/gaemimarble/game/entity/TypeConstants.java index 53101aa..dae9aa0 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/entity/TypeConstants.java +++ b/be/src/main/java/codesquad/gaemimarble/game/entity/TypeConstants.java @@ -20,4 +20,5 @@ public final class TypeConstants { public static final String GOLD_CARD = "goldCard"; public static final String ERROR = "error"; public static final String ROB = "rob"; + public static final String GAME_OVER = "gameOver"; } diff --git a/be/src/main/java/codesquad/gaemimarble/game/service/GameService.java b/be/src/main/java/codesquad/gaemimarble/game/service/GameService.java index 016239e..40d75f8 100644 --- a/be/src/main/java/codesquad/gaemimarble/game/service/GameService.java +++ b/be/src/main/java/codesquad/gaemimarble/game/service/GameService.java @@ -1,6 +1,7 @@ package codesquad.gaemimarble.game.service; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -9,6 +10,7 @@ import org.springframework.stereotype.Service; +import codesquad.gaemimarble.exception.CustomException; import codesquad.gaemimarble.game.dto.GameMapper; import codesquad.gaemimarble.game.dto.request.GameEndTurnRequest; import codesquad.gaemimarble.game.dto.request.GameEventResultRequest; @@ -32,6 +34,8 @@ import codesquad.gaemimarble.game.dto.response.GamePrisonDiceResponse; import codesquad.gaemimarble.game.dto.response.GameReadyResponse; import codesquad.gaemimarble.game.dto.response.GameRoomCreateResponse; +import codesquad.gaemimarble.game.dto.response.PlayerAsset; +import codesquad.gaemimarble.game.dto.response.UserRankingResponse; import codesquad.gaemimarble.game.dto.response.generalStatusBoard.GameStatusBoardResponse; import codesquad.gaemimarble.game.dto.response.userStatusBoard.GameUserBoardResponse; import codesquad.gaemimarble.game.entity.Board; @@ -201,7 +205,7 @@ public GameStatusBoardResponse proceedEvent(String eventName, Long gameId) { } } if (eventToProceed == null) { - throw new RuntimeException("이벤트 이름이 맞지 않습니다"); + throw new CustomException("이벤트 이름이 맞지 않습니다", null, gameId); } GameStatus gameStatus = gameRepository.getGameStatus(gameId); Map impactMap = eventToProceed.getImpact(); @@ -215,6 +219,7 @@ public GameStatusBoardResponse proceedEvent(String eventName, Long gameId) { } } updatePlayersAsset(gameStatus.getPlayers(), stockList); + gameStatus.incrementRoundCount(); return createGameStatusBoardResponse(gameId); } @@ -245,10 +250,12 @@ public GameUserBoardResponse buyStock(GameStockBuyRequest gameStockBuyRequest) { .stream() .filter(s -> s.getName().equals(gameStockBuyRequest.getStockName())) .findFirst() - .orElseThrow(() -> new RuntimeException("존재하지 않는 주식이름입니다")); + .orElseThrow(() -> new CustomException("존재하지 않는 주식이름입니다", gameStockBuyRequest.getPlayerId(), + gameStockBuyRequest.getGameId())); if (stock.getRemainingStock() < gameStockBuyRequest.getQuantity() | player.getCashAsset() < stock.getCurrentPrice() * gameStockBuyRequest.getQuantity()) { - throw new RuntimeException("구매할 수량이 부족하거나, 플레이어 보유 캐쉬가 부족합니다"); + throw new CustomException("구매할 수량이 부족하거나, 플레이어 보유 캐쉬가 부족합니다", gameStockBuyRequest.getPlayerId(), + gameStockBuyRequest.getGameId()); } player.buy(stock, gameStockBuyRequest.getQuantity()); stock.decrementQuantity(gameStockBuyRequest.getQuantity()); @@ -281,7 +288,8 @@ public GameUserBoardResponse sellStock(GameSellStockRequest gameSellStockRequest } for (String stockName : sellingStockInfoMap.keySet()) { if (player.getMyStocks().get(stockName) < sellingStockInfoMap.get(stockName)) { - throw new RuntimeException("플레이어가 보유한 주식보다 더 많이 팔수는 없습니다"); + throw new CustomException("플레이어가 보유한 주식보다 더 많이 팔수는 없습니다", gameSellStockRequest.getPlayerId(), + gameSellStockRequest.getGameId()); } } @@ -323,13 +331,15 @@ public GameEndTurnResponse endTurn(GameEndTurnRequest gameEndTurnRequest) { currentPlayerInfo.update(player); } } + return GameEndTurnResponse.builder().nextPlayerId(null).build(); } public void teleport(GameTeleportRequest gameTeleportRequest) { Player player = gameRepository.getPlayer(gameTeleportRequest.getGameId(), gameTeleportRequest.getPlayerId()); if (gameTeleportRequest.getLocation().equals(player.getLocation()) && player.getLocation() == 18) { - throw new RuntimeException("순간이동 칸으로 이동 할 수 없습니다"); + throw new CustomException("순간이동 칸으로 이동 할 수 없습니다", gameTeleportRequest.getPlayerId(), + gameTeleportRequest.getGameId()); } player.setLocation( gameTeleportRequest.getLocation() > player.getLocation() ? gameTeleportRequest.getLocation() : @@ -341,7 +351,7 @@ public GameStatusBoardResponse increaseCompanyStock(Long gameId, Integer locatio String shareName = gameStatus.getBoard().getBoard().get(location); Stock stock = gameStatus.getStocks().stream() .filter(s -> s.getName().equals(shareName)).findFirst() - .orElseThrow(() -> new RuntimeException("존재하지 않는 주식입니다.")); + .orElseThrow(() -> new CustomException("존재하지 않는 주식입니다.", null, gameId)); if (stock.getWasBought()) { stock.changePrice(10); } @@ -408,4 +418,17 @@ public List rob(GameRobRequest gameRobRequest) { target.addCashAsset(-10_000_000); return List.of(taker, target); } + + public boolean checkGameOver(Long gameId) { + GameStatus gameStatus = gameRepository.getGameStatus(gameId); + return gameStatus.getRoundCount() > 15; + } + + public UserRankingResponse createUserRanking(Long gameId) { + return UserRankingResponse.builder().ranking(gameRepository.getAllPlayer(gameId) + .stream() + .sorted(Comparator.comparing(Player::getTotalAsset).reversed()) + .map(p -> PlayerAsset.builder().playerId(p.getPlayerId()).totalAsset(p.getTotalAsset()).build()) + .collect(Collectors.toList())).build(); + } }