From 27d7dddd5c92437f49225073af228d34709c9665 Mon Sep 17 00:00:00 2001 From: Kevin Schmidt Date: Mon, 10 Mar 2025 13:39:14 +0100 Subject: [PATCH] TD-11: Handling and Validation of Tower Placement --- .../match/InvalidPlacementException.java | 11 +++ .../de/towerdefence/server/match/Match.java | 43 ++++++++- .../server/match/MatchService.java | 9 ++ .../channels/match/MatchWebsocketHandler.java | 69 ++++++++++----- .../channels/match/placing/GamePlayerMap.java | 13 +++ .../placing/InvalidPlacementMessage.java | 30 +++++++ .../match/placing/InvalidPlacementReason.java | 13 +++ .../placing/RequestTowerPlacingMessage.java | 16 ++++ .../server/channels/match/placing/Tower.java | 4 + .../match/placing/TowerPlacedMessage.java | 30 +++++++ ws/ws.yml | 87 ++++++++++++++++++- 11 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 src/main/java/de/towerdefence/server/match/InvalidPlacementException.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/GamePlayerMap.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementReason.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/RequestTowerPlacingMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/Tower.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java diff --git a/src/main/java/de/towerdefence/server/match/InvalidPlacementException.java b/src/main/java/de/towerdefence/server/match/InvalidPlacementException.java new file mode 100644 index 0000000..ede36f3 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/InvalidPlacementException.java @@ -0,0 +1,11 @@ +package de.towerdefence.server.match; + +import de.towerdefence.server.server.channels.match.placing.InvalidPlacementReason; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class InvalidPlacementException extends RuntimeException { + private final InvalidPlacementReason reason; +} diff --git a/src/main/java/de/towerdefence/server/match/Match.java b/src/main/java/de/towerdefence/server/match/Match.java index 491ec9e..7294b7f 100644 --- a/src/main/java/de/towerdefence/server/match/Match.java +++ b/src/main/java/de/towerdefence/server/match/Match.java @@ -1,7 +1,8 @@ package de.towerdefence.server.match; -import de.towerdefence.server.match.confirmation.ConfirmationCallbacks; import de.towerdefence.server.player.Player; +import de.towerdefence.server.server.channels.match.placing.InvalidPlacementReason; +import de.towerdefence.server.server.channels.match.placing.Tower; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,7 +10,47 @@ import lombok.Getter; @Getter public class Match { + public final static int MAP_SIZE_X = 10; + public final static int MAP_SIZE_Y = 20; private final String matchId; private final Player player1; private final Player player2; + private final Tower[][] player1Map = new Tower[MAP_SIZE_X][MAP_SIZE_Y]; + private final Tower[][] player2Map = new Tower[MAP_SIZE_X][MAP_SIZE_Y]; + + public Player getOpponent(Player player) { + boolean isPlayer1 = player1.equals(player); + boolean isPlayer2 = player2.equals(player); + if (!isPlayer1 && !isPlayer2) { + return null; + } + if (isPlayer1) { + return player2; + } + return player1; + } + + /** + * @return opponent + */ + public Player addTower(Player player, Tower tower, int x, int y) throws InvalidPlacementException { + if (player != player1 && player != player2) { + return null; + } + if (x < 0 || y < 0 || x + 1 > Match.MAP_SIZE_X || y + 1 > Match.MAP_SIZE_Y) { + throw new InvalidPlacementException(InvalidPlacementReason.OUT_OF_BOUNDS); + } + if (player == player1) { + if (player1Map[x][y] != null) { + throw new InvalidPlacementException(InvalidPlacementReason.LOCATION_USED); + } + player1Map[x][y] = tower; + } else { + if (player2Map[x][y] != null) { + throw new InvalidPlacementException(InvalidPlacementReason.LOCATION_USED); + } + player2Map[x][y] = tower; + } + return getOpponent(player); + } } diff --git a/src/main/java/de/towerdefence/server/match/MatchService.java b/src/main/java/de/towerdefence/server/match/MatchService.java index 3c55d9f..d547941 100644 --- a/src/main/java/de/towerdefence/server/match/MatchService.java +++ b/src/main/java/de/towerdefence/server/match/MatchService.java @@ -1,6 +1,7 @@ package de.towerdefence.server.match; import de.towerdefence.server.player.Player; +import de.towerdefence.server.server.channels.match.placing.Tower; import org.springframework.stereotype.Service; import java.util.HashMap; @@ -19,4 +20,12 @@ public class MatchService { public Match get(Player player) { return playerMatches.get(player); } + + /** + * @return opponent + */ + public Player placeTower(Player player, int x, int y) throws InvalidPlacementException { + return playerMatches.get(player).addTower(player, new Tower(), x, y); + + } } diff --git a/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java index 59986d7..aee3272 100644 --- a/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java @@ -1,20 +1,30 @@ package de.towerdefence.server.server.channels.match; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.towerdefence.server.match.InvalidPlacementException; import de.towerdefence.server.match.MatchService; +import de.towerdefence.server.player.Player; import de.towerdefence.server.server.JsonWebsocketHandler; +import de.towerdefence.server.server.channels.match.placing.GamePlayerMap; +import de.towerdefence.server.server.channels.match.placing.InvalidPlacementMessage; +import de.towerdefence.server.server.channels.match.placing.RequestTowerPlacingMessage; +import de.towerdefence.server.server.channels.match.placing.TowerPlacedMessage; import de.towerdefence.server.session.Channel; import de.towerdefence.server.session.SessionsService; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.Map; -import java.util.concurrent.*; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class MatchWebsocketHandler extends JsonWebsocketHandler { - final Map> sessionTaskMap = new ConcurrentHashMap<>(); - private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + protected final Map playerSessions = new ConcurrentHashMap<>(); private final MatchService matchService; + private final ObjectMapper objectMapper = new ObjectMapper(); public MatchWebsocketHandler(SessionsService sessionsService, MatchService matchService) { super(Channel.MATCH, sessionsService); @@ -24,28 +34,45 @@ public class MatchWebsocketHandler extends JsonWebsocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) { super.afterConnectionEstablished(session); - ScheduledFuture scheduledTask = scheduler.scheduleAtFixedRate( - () -> sendCurrentTime(session), - 0, - 1, - TimeUnit.SECONDS - ); - sessionTaskMap.put(session, scheduledTask); + playerSessions.put(sessionPlayers.get(session), session); } - private void sendCurrentTime(WebSocketSession session) { - ScheduledFuture task = sessionTaskMap.get(session); + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { try { - if (!session.isOpen()) { - throw new RuntimeException("Session is not open"); + String payload = message.getPayload(); + if (!Objects.equals( + objectMapper.readTree(payload).get("$id").asText(), + RequestTowerPlacingMessage.MESSAGE_ID + )) { + this.closeSession(session, CloseStatus.BAD_DATA); + return; } - new TimeMessage( - System.currentTimeMillis(), - matchService.get(this.sessionPlayers.get(session)).getMatchId() - ).send(session); - } catch (RuntimeException | IOException e) { - task.cancel(true); - sessionTaskMap.remove(session); + handleRequestTowerPlacingMessage(session, payload); + } catch (IOException ignored) { + this.closeSession(session, CloseStatus.BAD_DATA); } + + } + + private void handleRequestTowerPlacingMessage( + WebSocketSession session, + String payload + ) throws IOException { + RequestTowerPlacingMessage msg = objectMapper.readValue(payload, RequestTowerPlacingMessage.class); + Player opponent; + try { + opponent = this.matchService.placeTower( + this.sessionPlayers.get(session), + msg.getX(), + msg.getY() + ); + } catch (InvalidPlacementException exception) { + new InvalidPlacementMessage(msg.getX(), msg.getY(), exception.getReason()).send(session); + return; + } + WebSocketSession opponentSession = playerSessions.get(opponent); + new TowerPlacedMessage(msg.getX(), msg.getY(), GamePlayerMap.PLAYER).send(session); + new TowerPlacedMessage(msg.getX(), msg.getY(), GamePlayerMap.OPPONENT).send(opponentSession); } } diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/GamePlayerMap.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/GamePlayerMap.java new file mode 100644 index 0000000..0d59049 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/GamePlayerMap.java @@ -0,0 +1,13 @@ +package de.towerdefence.server.server.channels.match.placing; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GamePlayerMap { + PLAYER("player"), + OPPONENT("opponent"); + + private final String jsonName; +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java new file mode 100644 index 0000000..9570a47 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementMessage.java @@ -0,0 +1,30 @@ +package de.towerdefence.server.server.channels.match.placing; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class InvalidPlacementMessage extends JsonMessage { + + private final int x; + private final int y; + private final InvalidPlacementReason reason; + + @Override + protected String getMessageId() { + return "InvalidPlacement"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "x", factory.numberNode(this.x), + "y", factory.numberNode(this.y), + "reason", factory.textNode(this.reason.getJsonName()) + ); + } +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementReason.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementReason.java new file mode 100644 index 0000000..f049fe9 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/InvalidPlacementReason.java @@ -0,0 +1,13 @@ +package de.towerdefence.server.server.channels.match.placing; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum InvalidPlacementReason { + OUT_OF_BOUNDS("out-of-bounds"), + LOCATION_USED("location-used"); + + private final String jsonName; +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/RequestTowerPlacingMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/RequestTowerPlacingMessage.java new file mode 100644 index 0000000..ceab247 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/RequestTowerPlacingMessage.java @@ -0,0 +1,16 @@ +package de.towerdefence.server.server.channels.match.placing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@NotNull +public class RequestTowerPlacingMessage { + public static final String MESSAGE_ID = "RequestTowerPlacing"; + + @JsonProperty("$id") + private String messageId; + private int x; + private int y; +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/Tower.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/Tower.java new file mode 100644 index 0000000..d3008e9 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/Tower.java @@ -0,0 +1,4 @@ +package de.towerdefence.server.server.channels.match.placing; + +public class Tower { +} diff --git a/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java new file mode 100644 index 0000000..fcf3a31 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/match/placing/TowerPlacedMessage.java @@ -0,0 +1,30 @@ +package de.towerdefence.server.server.channels.match.placing; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class TowerPlacedMessage extends JsonMessage { + + private final int x; + private final int y; + private final GamePlayerMap map; + + @Override + protected String getMessageId() { + return "TowerPlaced"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "x", factory.numberNode(this.x), + "y", factory.numberNode(this.y), + "map", factory.textNode(this.map.getJsonName()) + ); + } +} diff --git a/ws/ws.yml b/ws/ws.yml index 36972e6..cb56075 100644 --- a/ws/ws.yml +++ b/ws/ws.yml @@ -178,8 +178,69 @@ channels: - $id - time - matchId - - + RequestTowerPlacing: + description: Requesting a placement of a tower + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + x: + type: integer + y: + type: integer + required: + - $id + - x + - y + TowerPlaced: + description: A tower was placed + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + x: + type: integer + y: + type: integer + map: + type: string + enum: + - player + - opponent + required: + - $id + - x + - y + - map + InvalidPlacement: + description: A tower was placed + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + x: + type: integer + y: + type: integer + reason: + type: string + enum: + - "out-of-bounds" + - "location-used" + required: + - $id + - x + - y + - reason operations: requestConnectionToken: @@ -241,6 +302,28 @@ operations: $ref: "#/channels/match" messages: - $ref: "#/channels/match/messages/CurrentUnixTime" + towerPlacing: + title: TowerPlacing + action: send + channel: + $ref: "#/channels/match" + messages: + - $ref: "#/channels/match/messages/RequestTowerPlacing" + reply: + channel: + $ref: "#/channels/match" + messages: + - $ref: "#/channels/match/messages/TowerPlaced" + - $ref: "#/channels/match/messages/InvalidPlacement" + enemyPlacedTower: + title: EnemyPlacedTower + action: receive + channel: + $ref: "#/channels/match" + messages: + - $ref: "#/channels/match/messages/TowerPlaced" + + components: securitySchemes: