From b0eeddcfd604394e684d680de87e7b4d1e301645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Sun, 14 Jul 2024 13:01:32 +0200 Subject: [PATCH 1/2] :sparkles: Only transmit tiles which are known to at least one robot --- .../poeschl/roborush/gamelogic/GameHandler.kt | 16 ++++- .../roborush/gamelogic/internal/MapHandler.kt | 18 ++++- .../poeschl/roborush/service/RobotService.kt | 4 +- .../roborush/gamelogic/GameHandlerTest.kt | 69 ++++++++++++++++--- .../gamelogic/internal/MapHandlerTest.kt | 34 ++++++++- .../roborush/service/RobotServiceTest.kt | 19 ++--- 6 files changed, 132 insertions(+), 28 deletions(-) diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt index 0a62daee..f1e1f06a 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt @@ -40,8 +40,9 @@ class GameHandler( private var currentTurn = 0 private var detectedIdleTurns = 0 + @Cacheable("currentMap") fun getCurrentMap(): Map { - return mapHandler.getCurrentMap() + return mapHandler.getMapWithPositions(getGlobalKnownPositions()) } fun getTileAtPosition(position: Position): Tile { @@ -118,6 +119,7 @@ class GameHandler( } } + @CacheEvict(cacheNames = ["robotKnownPosition", "globalKnownPositions", "currentMap"], allEntries = true) fun executeAllRobotActions() { robotHandler.executeRobotActions(this) setGameTurn(currentTurn + 1) @@ -172,7 +174,7 @@ class GameHandler( gameEnd = configService.getDurationSetting(SettingKey.TIMEOUT_GAME_END).value.inWholeMilliseconds ), nameOfWinningRobot = robotHandler.getWinningRobot()?.user?.username, - mapSize = mapHandler.getCurrentMap().size, + mapSize = mapHandler.getCurrentFullMap().size, fullMapScanPossible = configService.getBooleanSetting(SettingKey.ENABLE_FULL_MAP_SCAN).value ) } @@ -195,4 +197,14 @@ class GameHandler( } fun isFullMapScanPossible() = configService.getBooleanSetting(SettingKey.ENABLE_FULL_MAP_SCAN).value + + @Cacheable("robotKnownPosition", key = "robotId") + fun getKnownPositionsForRobot(robotId: Long): Set? { + return robotHandler.getActiveRobot(robotId)?.knownPositions + } + + @Cacheable("globalKnownPositions") + fun getGlobalKnownPositions(): Set { + return robotHandler.getAllActiveRobots().map { it.knownPositions }.flatten().toSet() + } } diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandler.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandler.kt index 865cd21f..0bd129a2 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandler.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandler.kt @@ -22,10 +22,26 @@ class MapHandler { private const val TILE_SCAN_COST = 0.1 } - fun getCurrentMap(): Map { + fun getCurrentFullMap(): Map { return currentMap } + fun getMapWithPositions(positions: Set): Map { + val reducedMap = Map( + currentMap.id, + currentMap.mapName, + currentMap.size, + currentMap.possibleStartPositions, + currentMap.targetPosition, + currentMap.maxRobotFuel, + currentMap.solarChargeRate, + currentMap.active + ) + + positions.forEach { pos -> reducedMap.addTile(getTileAtPosition(pos)) } + return reducedMap + } + fun getStartPositions(): List { return currentMap.possibleStartPositions } diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/service/RobotService.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/service/RobotService.kt index 95d0d9ed..1d8c94a6 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/service/RobotService.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/service/RobotService.kt @@ -72,10 +72,10 @@ class RobotService(private val robotRepository: RobotRepository, private val gam } fun getKnownPositionsForRobot(robotId: Long): Set { - return gameHandler.getActiveRobot(robotId)?.knownPositions ?: throw RobotNotActiveException("No active Robot found") + return gameHandler.getKnownPositionsForRobot(robotId) ?: throw RobotNotActiveException("No active Robot found") } fun getKnownPositionsForAllRobots(): Set { - return gameHandler.getActiveRobots().map { it.knownPositions }.flatten().toSet() + return gameHandler.getGlobalKnownPositions() } } diff --git a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt index 05dd7553..c630594a 100644 --- a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt +++ b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt @@ -21,7 +21,6 @@ import xyz.poeschl.roborush.service.ConfigService import xyz.poeschl.roborush.service.MapService import xyz.poeschl.roborush.service.PlayedGamesService import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.a -import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.listWithOne import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.setWithOne import xyz.poeschl.roborush.test.utils.builder.ConfigTypes.Companion.`$BooleanSetting` import xyz.poeschl.roborush.test.utils.builder.ConfigTypes.Companion.`$DurationSetting` @@ -50,11 +49,15 @@ class GameHandlerTest { @Test fun getCurrentMap() { // WHEN - val tiles = listWithOne(`$Tile`()) + val tiles = listOf(a(`$Tile`()), a(`$Tile`()), a(`$Tile`())) + val positions = tiles.map { it.position }.toSet() val map = a(`$Map`()) tiles.forEach { map.addTile(it) } + val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positions)) + val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positions)) - every { mapHandler.getCurrentMap() } returns map + every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) + every { mapHandler.getMapWithPositions(positions) } returns map // THEN val result = gameHandler.getCurrentMap() @@ -343,7 +346,7 @@ class GameHandlerTest { every { gameStateMachine.getCurrentState() } returns state every { mapHandler.getTargetPosition() } returns target - every { mapHandler.getCurrentMap() } returns map + every { mapHandler.getCurrentFullMap() } returns map every { mapHandler.isSolarChargePossible() } returns chargingPossible every { robotHandler.getWinningRobot() } returns robot every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(true)) @@ -377,7 +380,7 @@ class GameHandlerTest { every { gameStateMachine.getCurrentState() } returns state every { mapHandler.getTargetPosition() } returns target every { mapHandler.isSolarChargePossible() } returns chargingPossible - every { mapHandler.getCurrentMap() } returns map + every { mapHandler.getCurrentFullMap() } returns map every { robotHandler.getWinningRobot() } returns robot every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns BooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO, false) every { configService.getDurationSetting(SettingKey.TIMEOUT_WAIT_FOR_PLAYERS) } returns a(`$DurationSetting`()) @@ -447,7 +450,7 @@ class GameHandlerTest { val map = a(`$Map`()) every { robotHandler.getAllActiveRobots() } returns robots - every { mapHandler.getCurrentMap() } returns map + every { mapHandler.getCurrentFullMap() } returns map // THEN gameHandler.executeAllRobotActions() @@ -465,7 +468,7 @@ class GameHandlerTest { every { mapService.getNextChallengeMap() } returns map every { robotHandler.getAllActiveRobots() } returns robots - every { mapHandler.getCurrentMap() } returns map + every { mapHandler.getCurrentFullMap() } returns map gameHandler.executeAllRobotActions() val previousTurn = getCurrentTurn() @@ -479,6 +482,56 @@ class GameHandlerTest { assertThat(currentTurn).isEqualTo(0) } + @Test + fun getKnownPositionsForRobot() { + // WHEN + val robotId = 1L + val knownPositions = setOf(a(`$Position`())) + val activeRobot = a(`$ActiveRobot`().withId(robotId).withKnownPositions(knownPositions)) + + every { robotHandler.getActiveRobot(robotId) } returns activeRobot + + // THEN + val result = gameHandler.getKnownPositionsForRobot(robotId) + + // VERIFY + assertThat(result).isEqualTo(knownPositions) + } + + @Test + fun getKnownPositionsForRobot_notActive() { + // WHEN + val robotId = 1L + + every { robotHandler.getActiveRobot(robotId) } returns null + + // THEN + val result = gameHandler.getKnownPositionsForRobot(robotId) + + // VERIFY + assertThat(result).isNull() + } + + @Test + fun getKnownPositionsForAllRobots() { + // WHEN + val overlappingPos = a(`$Position`()) + val positionsA = setOf(a(`$Position`()), overlappingPos) + val positionsB = setOf(a(`$Position`()), overlappingPos) + val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positionsA)) + val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positionsB)) + + every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) + + // THEN + val result = gameHandler.getGlobalKnownPositions() + + // VERIFY + assertThat(result).containsAll(positionsA) + assertThat(result).containsAll(positionsB) + assertThat(result).containsOnlyOnce(overlappingPos) + } + private fun getCurrentTurn(): Int { val state = a(`$GameState`()) val target = a(`$Position`()) @@ -489,7 +542,7 @@ class GameHandlerTest { every { gameStateMachine.getCurrentState() } returns state every { mapHandler.getTargetPosition() } returns target every { mapHandler.isSolarChargePossible() } returns chargingPossible - every { mapHandler.getCurrentMap() } returns map + every { mapHandler.getCurrentFullMap() } returns map every { robotHandler.getWinningRobot() } returns robot every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns BooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO, false) every { configService.getDurationSetting(SettingKey.TIMEOUT_WAIT_FOR_PLAYERS) } returns a(`$DurationSetting`()) diff --git a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandlerTest.kt b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandlerTest.kt index 2b309a7b..923f8970 100644 --- a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandlerTest.kt +++ b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/internal/MapHandlerTest.kt @@ -10,6 +10,8 @@ import xyz.poeschl.roborush.models.Size import xyz.poeschl.roborush.models.TileType import xyz.poeschl.roborush.repositories.Map import xyz.poeschl.roborush.repositories.Tile +import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.a +import xyz.poeschl.roborush.test.utils.builder.GameLogicBuilder.Companion.`$Position` import java.util.stream.IntStream import java.util.stream.Stream import kotlin.math.ceil @@ -184,6 +186,36 @@ class MapHandlerTest { assertThat(mapHandler.getTileAtPosition(Position(3, 2))).isEqualTo(Tile(null, Position(3, 2), 12, TileType.TARGET_TILE)) } + @Test + fun getMapWithPositions() { + // WHEN + // Map: + // 1 2 + // 3 4 + val map = createNewPresetMap(Size(2, 2), listOf(1, 2, 3, 4), Position(0, 0)) + mapHandler.loadNewMap(map) + val positions = setOf( + a(`$Position`().withX(0).withY(0)), + a(`$Position`().withX(1).withY(1)) + ) + + // THEN + val cutOfMap = mapHandler.getMapWithPositions(positions) + + // VERIFY + // Make sure we only have the 2 tiles + assertThat(cutOfMap.mapData.map { it.position }).containsAll(positions) + + assertThat(cutOfMap.id).isEqualTo(map.id) + assertThat(cutOfMap.mapName).isEqualTo(map.mapName) + assertThat(cutOfMap.size).isEqualTo(map.size) + assertThat(cutOfMap.possibleStartPositions).isEqualTo(map.possibleStartPositions) + assertThat(cutOfMap.targetPosition).isEqualTo(map.targetPosition) + assertThat(cutOfMap.maxRobotFuel).isEqualTo(map.maxRobotFuel) + assertThat(cutOfMap.solarChargeRate).isEqualTo(map.solarChargeRate) + assertThat(cutOfMap.active).isEqualTo(map.active) + } + private fun createNewRandomMap(size: Size): Map { val randomHeights = IntStream.range(0, size.width * size.height) .map { Random.nextInt(0, 8) }.toList() @@ -195,7 +227,7 @@ class MapHandlerTest { } private fun createNewPresetMap(size: Size, heights: List, start: Position, target: Position = Position(size.width - 1, size.height - 1)): Map { - val map = Map(null, "gen", size, listOf(start), target, 100) + val map = Map(1L, "gen", size, listOf(start), target, 100) createHeightMap(size, heights, listOf(start), target).forEach { map.addTile(it) } return map } diff --git a/backend/src/test/kotlin/xyz/poeschl/roborush/service/RobotServiceTest.kt b/backend/src/test/kotlin/xyz/poeschl/roborush/service/RobotServiceTest.kt index f83c1d8b..48cc1856 100644 --- a/backend/src/test/kotlin/xyz/poeschl/roborush/service/RobotServiceTest.kt +++ b/backend/src/test/kotlin/xyz/poeschl/roborush/service/RobotServiceTest.kt @@ -227,9 +227,8 @@ class RobotServiceTest { // WHEN val robotId = 1L val knownPositions = setOf(a(`$Position`())) - val activeRobot = a(`$ActiveRobot`().withId(robotId).withKnownPositions(knownPositions)) - every { gameHandler.getActiveRobot(robotId) } returns activeRobot + every { gameHandler.getKnownPositionsForRobot(robotId) } returns knownPositions // THEN val result = robotService.getKnownPositionsForRobot(robotId) @@ -242,10 +241,8 @@ class RobotServiceTest { fun getKnownPositionsForRobot_notActive() { // WHEN val robotId = 1L - val knownPositions = setOf(a(`$Position`())) - val activeRobot = a(`$ActiveRobot`().withId(robotId).withKnownPositions(knownPositions)) - every { gameHandler.getActiveRobot(robotId) } returns null + every { gameHandler.getKnownPositionsForRobot(robotId) } returns null // THEN assertThrows { @@ -256,20 +253,14 @@ class RobotServiceTest { @Test fun getKnownPositionsForAllRobots() { // WHEN - val overlappingPos = a(`$Position`()) - val positionsA = setOf(a(`$Position`()), overlappingPos) - val positionsB = setOf(a(`$Position`()), overlappingPos) - val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positionsA)) - val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positionsB)) + val positions = setOf(a(`$Position`()), a(`$Position`())) - every { gameHandler.getActiveRobots() } returns setOf(activeRobot1, activeRobot2) + every { gameHandler.getGlobalKnownPositions() } returns positions // THEN val result = robotService.getKnownPositionsForAllRobots() // VERIFY - assertThat(result).containsAll(positionsA) - assertThat(result).containsAll(positionsB) - assertThat(result).containsOnlyOnce(overlappingPos) + assertThat(result).containsAll(positions) } } From 88cf333f3cfef7b1f6931df2fae1674101ce9136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Sun, 28 Jul 2024 11:52:17 +0200 Subject: [PATCH 2/2] :sparkles: Send mapdata on every update with canvas optimize --- .../roborush/controller/GameRestController.kt | 6 +- .../controller/WebsocketController.kt | 6 +- .../roborush/controller/restmodels/MapDtos.kt | 34 +++++++++ .../poeschl/roborush/gamelogic/GameHandler.kt | 30 ++++++-- .../xyz/poeschl/roborush/GamePlayTest.kt | 2 + .../roborush/gamelogic/GameHandlerTest.kt | 76 ++++++++++++++++++- .../src/components/MapCanvasComponent.vue | 27 ++----- frontend/src/models/Map.ts | 2 + frontend/src/services/WebsocketService.ts | 12 +-- frontend/src/stores/GameStore.ts | 23 +++--- frontend/src/views/FullScreenView.vue | 11 +-- frontend/src/views/MainView.vue | 11 +-- 12 files changed, 162 insertions(+), 78 deletions(-) diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/GameRestController.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/GameRestController.kt index 6abdc83d..eb1b0846 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/GameRestController.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/GameRestController.kt @@ -8,17 +8,17 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import xyz.poeschl.roborush.configuration.OpenApiConfig +import xyz.poeschl.roborush.controller.restmodels.PlaygroundMap import xyz.poeschl.roborush.gamelogic.GameHandler import xyz.poeschl.roborush.models.Game -import xyz.poeschl.roborush.repositories.Map @RestController @RequestMapping("/game") class GameRestController(private val gameHandler: GameHandler) { @GetMapping("/map", produces = [MediaType.APPLICATION_JSON_VALUE]) - fun getMap(): Map { - return gameHandler.getCurrentMap() + fun getMap(): PlaygroundMap { + return gameHandler.getCurrentPlaygroundMap() } @Operation( diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/WebsocketController.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/WebsocketController.kt index c6a3fe50..d3439015 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/WebsocketController.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/WebsocketController.kt @@ -4,9 +4,9 @@ import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.stereotype.Controller import xyz.poeschl.roborush.gamelogic.GameState import xyz.poeschl.roborush.models.ActiveRobot -import xyz.poeschl.roborush.models.Position import xyz.poeschl.roborush.models.PublicRobot import xyz.poeschl.roborush.models.settings.ClientSettings +import xyz.poeschl.roborush.repositories.Map @Controller class WebsocketController(private val messageTemplate: SimpMessagingTemplate) { @@ -37,7 +37,7 @@ class WebsocketController(private val messageTemplate: SimpMessagingTemplate) { messageTemplate.convertAndSendToUser(user, "/queue/robot/knownPositions", robot.knownPositions) } - fun sendGlobalKnownPositionsUpdate(knownPositions: Set) { - messageTemplate.convertAndSend("/topic/robot/knownPositions", knownPositions) + fun sendMapTileUpdate(map: Map) { + messageTemplate.convertAndSend("/topic/map/tiles", map.mapData) } } diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/restmodels/MapDtos.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/restmodels/MapDtos.kt index 3663a438..ef742a9c 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/restmodels/MapDtos.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/restmodels/MapDtos.kt @@ -1,7 +1,41 @@ package xyz.poeschl.roborush.controller.restmodels +import xyz.poeschl.roborush.models.Position +import xyz.poeschl.roborush.models.Size +import xyz.poeschl.roborush.repositories.Map +import xyz.poeschl.roborush.repositories.Tile + data class MapGenerationResult(val warnings: List) data class MapActiveDto(val active: Boolean) data class MapAttributeSaveDto(val mapName: String, val maxRobotFuel: Int, val solarChargeRate: Double) + +data class PlaygroundMap( + val id: Long, + val mapName: String, + val size: Size, + val possibleStartPositions: List, + val targetPosition: Position, + val maxRobotFuel: Int = 300, + val solarChargeRate: Double = 0.0, + val active: Boolean = false, + val mapData: List = mutableListOf(), + val minHeight: Int, + val maxHeight: Int +) { + + constructor(map: Map, minHeight: Int, maxHeight: Int) : this( + map.id!!, + map.mapName, + map.size, + map.possibleStartPositions, + map.targetPosition, + map.maxRobotFuel, + map.solarChargeRate, + map.active, + map.mapData, + minHeight, + maxHeight + ) +} diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt index f1e1f06a..48bd0fce 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/gamelogic/GameHandler.kt @@ -5,6 +5,7 @@ import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.Cacheable import xyz.poeschl.roborush.configuration.GameLogic import xyz.poeschl.roborush.controller.WebsocketController +import xyz.poeschl.roborush.controller.restmodels.PlaygroundMap import xyz.poeschl.roborush.exceptions.PositionNotAllowedException import xyz.poeschl.roborush.exceptions.PositionOutOfMapException import xyz.poeschl.roborush.gamelogic.actions.RobotAction @@ -40,11 +41,21 @@ class GameHandler( private var currentTurn = 0 private var detectedIdleTurns = 0 - @Cacheable("currentMap") + @Cacheable("knownMap") fun getCurrentMap(): Map { return mapHandler.getMapWithPositions(getGlobalKnownPositions()) } + @Cacheable("playgroundMap") + fun getCurrentPlaygroundMap(): PlaygroundMap { + val fullMap = mapHandler.getCurrentFullMap() + + val minHeight = fullMap.mapData.minOf { it.height } + val maxHeight = fullMap.mapData.maxOf { it.height } + + return PlaygroundMap(getCurrentMap(), minHeight, maxHeight) + } + fun getTileAtPosition(position: Position): Tile { return mapHandler.getTileAtPosition(position) } @@ -81,7 +92,6 @@ class GameHandler( websocketController.sendRobotUpdate(activeRobot) websocketController.sendUserRobotData(activeRobot) websocketController.sendKnownPositionsUpdate(activeRobot) - websocketController.sendGlobalKnownPositionsUpdate(robotHandler.getAllActiveRobots().map { it.knownPositions }.flatten().toSet()) } fun getFuelCostForMove(current: Position, next: Position): Int { @@ -119,10 +129,11 @@ class GameHandler( } } - @CacheEvict(cacheNames = ["robotKnownPosition", "globalKnownPositions", "currentMap"], allEntries = true) + @CacheEvict(cacheNames = ["knownMap", "playgroundMap"], allEntries = true) fun executeAllRobotActions() { robotHandler.executeRobotActions(this) setGameTurn(currentTurn + 1) + websocketController.sendMapTileUpdate(getCurrentMap()) } fun registerRobotForNextGame(robotId: Long) { @@ -138,7 +149,7 @@ class GameHandler( websocketController.sendRobotUpdate(registeredRobot) websocketController.sendUserRobotData(registeredRobot) websocketController.sendKnownPositionsUpdate(registeredRobot) - websocketController.sendGlobalKnownPositionsUpdate(robotHandler.getAllActiveRobots().map { it.knownPositions }.flatten().toSet()) + websocketController.sendMapTileUpdate(getCurrentMap()) } } else { throw PositionNotAllowedException("Could not place robot at a empty start position.") @@ -158,7 +169,6 @@ class GameHandler( robotHandler.setRobotMaxFuel(map.maxRobotFuel) robotHandler.clearActiveRobots() setGameTurn(0) - websocketController.sendGlobalKnownPositionsUpdate(emptySet()) } @Cacheable("gameInfoCache") @@ -198,13 +208,17 @@ class GameHandler( fun isFullMapScanPossible() = configService.getBooleanSetting(SettingKey.ENABLE_FULL_MAP_SCAN).value - @Cacheable("robotKnownPosition", key = "robotId") fun getKnownPositionsForRobot(robotId: Long): Set? { return robotHandler.getActiveRobot(robotId)?.knownPositions } - @Cacheable("globalKnownPositions") fun getGlobalKnownPositions(): Set { - return robotHandler.getAllActiveRobots().map { it.knownPositions }.flatten().toSet() + val positions = robotHandler.getAllActiveRobots().map { it.knownPositions }.flatten().toMutableSet() + + if (configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO).value) { + positions.add(mapHandler.getTargetPosition()) + } + + return positions } } diff --git a/backend/src/test/kotlin/xyz/poeschl/roborush/GamePlayTest.kt b/backend/src/test/kotlin/xyz/poeschl/roborush/GamePlayTest.kt index 68feca43..6c6ca13c 100644 --- a/backend/src/test/kotlin/xyz/poeschl/roborush/GamePlayTest.kt +++ b/backend/src/test/kotlin/xyz/poeschl/roborush/GamePlayTest.kt @@ -22,6 +22,7 @@ import xyz.poeschl.roborush.service.MapService import xyz.poeschl.roborush.service.PlayedGamesService import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.a import xyz.poeschl.roborush.test.utils.builder.Builders.Companion.listWithOne +import xyz.poeschl.roborush.test.utils.builder.ConfigTypes.Companion.`$BooleanSetting` import xyz.poeschl.roborush.test.utils.builder.ConfigTypes.Companion.`$IntSetting` import xyz.poeschl.roborush.test.utils.builder.GameLogicBuilder.Companion.`$Map` import xyz.poeschl.roborush.test.utils.builder.GameLogicBuilder.Companion.`$Position` @@ -53,6 +54,7 @@ class GamePlayTest { every { mapService.getNextChallengeMap() } returns createMapWithFuelStation() every { robotRepository.findById(robot.id!!) } returns Optional.of(robot) every { configService.getIntSetting(SettingKey.DISTANCE_ROBOT_SIGHT_ON_MOVE) } returns a(`$IntSetting`().withValue(1)) + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`()) gameStateMachine.setGameState(GameState.PREPARE) gameHandler.prepareNewGame() diff --git a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt index c630594a..ed465439 100644 --- a/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt +++ b/backend/src/test/kotlin/xyz/poeschl/roborush/gamelogic/GameHandlerTest.kt @@ -58,6 +58,7 @@ class GameHandlerTest { every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) every { mapHandler.getMapWithPositions(positions) } returns map + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(false)) // THEN val result = gameHandler.getCurrentMap() @@ -67,6 +68,37 @@ class GameHandlerTest { assertThat(result.mapData).containsAll(tiles) } + @Test + fun getCurrentPlaygroundMap() { + // WHEN + val tiles = listOf(a(`$Tile`().withHeight(10)), a(`$Tile`().withHeight(50)), a(`$Tile`().withHeight(20))) + val positions = tiles.map { it.position }.toSet() + val map = a(`$Map`().withId(2)) + tiles.forEach { map.addTile(it) } + val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positions)) + val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positions)) + + every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) + every { mapHandler.getCurrentFullMap() } returns map + every { mapHandler.getMapWithPositions(positions) } returns map + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(false)) + + // THEN + val result = gameHandler.getCurrentPlaygroundMap() + + // VERIFY + assertThat(result.id).isEqualTo(map.id) + assertThat(result.mapName).isEqualTo(map.mapName) + assertThat(result.possibleStartPositions).isEqualTo(map.possibleStartPositions) + assertThat(result.targetPosition).isEqualTo(map.targetPosition) + assertThat(result.active).isEqualTo(map.active) + assertThat(result.maxRobotFuel).isEqualTo(map.maxRobotFuel) + assertThat(result.solarChargeRate).isEqualTo(map.solarChargeRate) + assertThat(result.mapData).containsAll(tiles) + assertThat(result.minHeight).isEqualTo(10) + assertThat(result.maxHeight).isEqualTo(50) + } + @Test fun isPositionValidForMove() { // WHEN @@ -159,7 +191,6 @@ class GameHandlerTest { verify { websocketController.sendRobotUpdate(robot) } verify { websocketController.sendUserRobotData(robot) } verify { websocketController.sendKnownPositionsUpdate(robot) } - verify { websocketController.sendGlobalKnownPositionsUpdate(knownPositions) } } @Test @@ -237,6 +268,11 @@ class GameHandlerTest { @Test fun executeAllRobotActions() { // WHEN + val robots = setWithOne(`$ActiveRobot`()) + + every { robotHandler.getAllActiveRobots() } returns robots + every { mapHandler.getMapWithPositions(any()) } returns a(`$Map`()) + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(false)) // THEN gameHandler.executeAllRobotActions() @@ -254,13 +290,17 @@ class GameHandlerTest { val registeredRobot = a(`$ActiveRobot`()) val existingRobots = setWithOne(`$ActiveRobot`()) val neighborTiles = listOf(a(`$Tile`()), a(`$Tile`()), a(`$Tile`())) + val map = a(`$Map`()) every { mapHandler.getStartPositions() } returns possibleStart every { mapHandler.getTilesInDistance(startPosition, 2) } returns Pair(neighborTiles, 0) + every { mapHandler.getTargetPosition() } returns a(`$Position`()) + every { mapHandler.getMapWithPositions(any()) } returns map every { robotHandler.getACurrentlyFreePosition(possibleStart) } returns startPosition every { robotHandler.registerRobotForGame(1, startPosition) } returns registeredRobot every { robotHandler.getAllActiveRobots() } returns existingRobots every { configService.getIntSetting(SettingKey.DISTANCE_ROBOT_SIGHT_ON_MOVE) } returns a(`$IntSetting`().withValue(2)) + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`()) // THEN gameHandler.registerRobotForNextGame(1) @@ -271,7 +311,7 @@ class GameHandlerTest { verify { robotHandler.registerRobotForGame(1, startPosition) } verify { websocketController.sendRobotUpdate(registeredRobot) } verify { websocketController.sendKnownPositionsUpdate(registeredRobot) } - verify { websocketController.sendGlobalKnownPositionsUpdate(any()) } + verify { websocketController.sendMapTileUpdate(map) } } @Test @@ -305,7 +345,6 @@ class GameHandlerTest { verify { mapHandler.loadNewMap(map) } verify { robotHandler.clearActiveRobots() } verify(exactly = 1) { websocketController.sendTurnUpdate(0) } - verify(exactly = 1) { websocketController.sendGlobalKnownPositionsUpdate(setOf()) } } @Test @@ -451,6 +490,7 @@ class GameHandlerTest { every { robotHandler.getAllActiveRobots() } returns robots every { mapHandler.getCurrentFullMap() } returns map + every { mapHandler.getMapWithPositions(any()) } returns map // THEN gameHandler.executeAllRobotActions() @@ -469,6 +509,8 @@ class GameHandlerTest { every { mapService.getNextChallengeMap() } returns map every { robotHandler.getAllActiveRobots() } returns robots every { mapHandler.getCurrentFullMap() } returns map + every { mapHandler.getMapWithPositions(any()) } returns map + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(false)) gameHandler.executeAllRobotActions() val previousTurn = getCurrentTurn() @@ -520,8 +562,35 @@ class GameHandlerTest { val positionsB = setOf(a(`$Position`()), overlappingPos) val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positionsA)) val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positionsB)) + val targetPosition = a(`$Position`()) every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) + every { mapHandler.getTargetPosition() } returns targetPosition + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(false)) + + // THEN + val result = gameHandler.getGlobalKnownPositions() + + // VERIFY + assertThat(result).containsAll(positionsA) + assertThat(result).containsAll(positionsB) + assertThat(result).containsOnlyOnce(overlappingPos) + assertThat(result).doesNotContain(targetPosition) + } + + @Test + fun getKnownPositionsForAllRobots_withKnownTarget() { + // WHEN + val overlappingPos = a(`$Position`()) + val positionsA = setOf(a(`$Position`()), overlappingPos) + val positionsB = setOf(a(`$Position`()), overlappingPos) + val activeRobot1 = a(`$ActiveRobot`().withKnownPositions(positionsA)) + val activeRobot2 = a(`$ActiveRobot`().withKnownPositions(positionsB)) + val targetPosition = a(`$Position`()) + + every { robotHandler.getAllActiveRobots() } returns setOf(activeRobot1, activeRobot2) + every { mapHandler.getTargetPosition() } returns targetPosition + every { configService.getBooleanSetting(SettingKey.TARGET_POSITION_IN_GAMEINFO) } returns a(`$BooleanSetting`().withValue(true)) // THEN val result = gameHandler.getGlobalKnownPositions() @@ -530,6 +599,7 @@ class GameHandlerTest { assertThat(result).containsAll(positionsA) assertThat(result).containsAll(positionsB) assertThat(result).containsOnlyOnce(overlappingPos) + assertThat(result).contains(targetPosition) } private fun getCurrentTurn(): Int { diff --git a/frontend/src/components/MapCanvasComponent.vue b/frontend/src/components/MapCanvasComponent.vue index 7a2184ef..1c3e11b8 100644 --- a/frontend/src/components/MapCanvasComponent.vue +++ b/frontend/src/components/MapCanvasComponent.vue @@ -86,9 +86,7 @@ onMounted(() => { if (props.pathToDisplay) { watch(() => props.pathToDisplay!.points, drawDisplayPath); } - if (props.positionsToDraw) { - watch(() => props.positionsToDraw?.data, drawMapTiles); - } + watch(() => props.map?.mapData, drawMapTiles); watch( () => props.drawablePath, (newValue) => { @@ -147,14 +145,14 @@ const onPathCanvasClick = (event: MouseEvent) => { }; const redraw = () => { - updateCanvasSize(); - drawMap(); + updateCanvasData(); + drawMapTiles(); drawRobots(); drawDisplayPath(); drawPathMarkers(); }; -const updateCanvasSize = () => { +const updateCanvasData = () => { if (props.map) { mapWidth.value = props.map.size.width * fullTileSize; mapHeight.value = props.map.size.height * fullTileSize; @@ -175,22 +173,9 @@ const updateCanvasSize = () => { displayPathDrawContext.value.canvas.width = mapWidth.value; displayPathDrawContext.value.canvas.height = mapHeight.value; } - } -}; - -const drawMap = () => { - const tiles = heightMap.value; - if (mapCanvas.value && mapDrawContext.value && tiles.length > 0) { - log.debug("Draw whole map"); - - const maxHeight = Math.max(...[...tiles].map((t) => t.height)); - const minHeight = Math.min(...[...tiles].map((t) => t.height)); - - log.debug("Max height: ", maxHeight, "Min height: ", minHeight); - mapTileMinHeight.value = minHeight; - mapTileMaxHeight.value = maxHeight; - drawMapTiles(); + mapTileMinHeight.value = props.map.minHeight; + mapTileMaxHeight.value = props.map.maxHeight; } }; diff --git a/frontend/src/models/Map.ts b/frontend/src/models/Map.ts index 00778b47..b86b790e 100644 --- a/frontend/src/models/Map.ts +++ b/frontend/src/models/Map.ts @@ -8,6 +8,8 @@ export interface PlaygroundMap { maxRobotFuel: number; solarChargeRate: number; mapData: Tile[]; + minHeight: number; + maxHeight: number; } export interface PlaygroundMapAttributes { diff --git a/frontend/src/services/WebsocketService.ts b/frontend/src/services/WebsocketService.ts index c2930b23..7e376d28 100644 --- a/frontend/src/services/WebsocketService.ts +++ b/frontend/src/services/WebsocketService.ts @@ -7,7 +7,7 @@ import { watch } from "vue"; import type { User } from "@/models/User"; import type { GameState } from "@/models/Game"; import log from "loglevel"; -import type { Position } from "@/models/Map"; +import type { Position, Tile } from "@/models/Map"; import type { ClientSettings } from "@/models/Config"; export enum WebSocketTopic { @@ -16,8 +16,8 @@ export enum WebSocketTopic { GAME_STATE_TOPIC, GAME_TURN_TOPIC, CLIENT_SETTINGS_TOPIC, - PUBLIC_KNOWN_POSITIONS_TOPIC, PRIVATE_KNOWN_POSITIONS_TOPIC, + MAP_TILES_UPDATE, } export function useWebSocket(): { initWebsocket: Function; registerForTopicCallback: Function } { @@ -25,8 +25,8 @@ export function useWebSocket(): { initWebsocket: Function; registerForTopicCallb const publicRobotUpdateTopic = "/topic/robot"; const publicGameStateUpdateTopic = "/topic/game/state"; const publicGameTurnUpdateTopic = "/topic/game/turn"; + const publicMapTileUpdateTopic = "/topic/map/tiles"; const publicClientSettingsTopic = "/topic/config/client"; - const publicKnownPositionsUpdateTopic = "/topic/robot/knownPositions"; const userRobotUpdateQueue = "/queue/robot"; const userRobotKnownPositionsUpdateTopic = "/queue/robot/knownPositions"; const topicListener = new Map(); @@ -122,9 +122,9 @@ export function useWebSocket(): { initWebsocket: Function; registerForTopicCallb const settings: ClientSettings = JSON.parse(message.body); topicListener.get(WebSocketTopic.CLIENT_SETTINGS_TOPIC)?.call(null, settings); }); - client.subscribe(publicKnownPositionsUpdateTopic, (message) => { - const knownPositions: Position[] = JSON.parse(message.body); - topicListener.get(WebSocketTopic.PUBLIC_KNOWN_POSITIONS_TOPIC)?.call(null, knownPositions); + client.subscribe(publicMapTileUpdateTopic, (message) => { + const tiles: Tile[] = JSON.parse(message.body); + topicListener.get(WebSocketTopic.MAP_TILES_UPDATE)?.call(null, tiles); }); }; diff --git a/frontend/src/stores/GameStore.ts b/frontend/src/stores/GameStore.ts index b26d376f..b56ca63e 100644 --- a/frontend/src/stores/GameStore.ts +++ b/frontend/src/stores/GameStore.ts @@ -2,7 +2,7 @@ import { defineStore } from "pinia"; import type { ComputedRef, Ref } from "vue"; import { computed, ref } from "vue"; import type { ActiveRobot, PublicRobot, ScoreboardEntry } from "@/models/Robot"; -import type { PlaygroundMap, Position } from "@/models/Map"; +import type { PlaygroundMap, Position, Tile } from "@/models/Map"; import { WebSocketTopic } from "@/services/WebsocketService"; import type { User } from "@/models/User"; import { useGameService } from "@/services/GameService"; @@ -21,7 +21,6 @@ export const useGameStore = defineStore("gameStore", () => { const currentGame = ref({ currentState: GameState.ENDED, currentTurn: 0, solarChargePossible: false, fullMapScanPossible: false }); const internalMap = ref(); - const globalKnownPositions: Ref<{ data: Position[] }> = ref({ data: [] }); // Needed workaround, since ref() don't detect updates on pure arrays. const internalRobots: Ref<{ data: PublicRobot[] }> = ref({ data: [] }); @@ -78,8 +77,8 @@ export const useGameStore = defineStore("gameStore", () => { websocketService.value.registerForTopicCallback(WebSocketTopic.PRIVATE_ROBOT_TOPIC, updateUserRobot); websocketService.value.registerForTopicCallback(WebSocketTopic.GAME_STATE_TOPIC, updateGameStateTo); websocketService.value.registerForTopicCallback(WebSocketTopic.GAME_TURN_TOPIC, updateGameTurnTo); - websocketService.value.registerForTopicCallback(WebSocketTopic.PUBLIC_KNOWN_POSITIONS_TOPIC, updateGlobalKnownPositionsTo); websocketService.value.registerForTopicCallback(WebSocketTopic.PRIVATE_KNOWN_POSITIONS_TOPIC, updateUserRobotKnownPositionsTo); + websocketService.value.registerForTopicCallback(WebSocketTopic.MAP_TILES_UPDATE, updateMapTiles); }; const updateRobot = (updatedRobot: PublicRobot) => { @@ -100,11 +99,6 @@ export const useGameStore = defineStore("gameStore", () => { const updateGameInfo = () => { gameService.getCurrentGame().then((gameInfo) => (currentGame.value = gameInfo)); - updateGlobalKnownPositions(); - }; - - const updateGlobalKnownPositions = () => { - robotService.getAllRobotsKnownPositions().then((positions) => updateGlobalKnownPositionsTo(positions)); }; const updateUserRobotKnownPositions = () => { @@ -127,7 +121,6 @@ export const useGameStore = defineStore("gameStore", () => { if (previousGameState == GameState.PREPARE && gameState === GameState.WAIT_FOR_PLAYERS) { // Update map data after game preparations updateMap(); - updateGlobalKnownPositions(); } if (gameState === GameState.ENDED) { updateScoreBoard(); @@ -139,14 +132,17 @@ export const useGameStore = defineStore("gameStore", () => { currentGame.value.currentTurn = turnCount; }; - const updateGlobalKnownPositionsTo = (positions: Position[]) => { - globalKnownPositions.value.data = positions; - }; - const updateUserRobotKnownPositionsTo = (positions: Position[]) => { userRobotKnownPositions.value.data = positions; }; + const updateMapTiles = (tiles: Tile[]) => { + if (internalMap.value != undefined && internalMap.value.mapData.length > 0) { + internalMap.value.mapData = []; + internalMap.value.mapData = tiles; + } + }; + const registerRobotOnGame = (): Promise => { return robotService.registerCurrentRobotForGame().then(() => { updateRobots(); @@ -197,7 +193,6 @@ export const useGameStore = defineStore("gameStore", () => { solarCharge, updateScoreBoard, scoreBoard, - globalKnownPositions, userRobotKnownPositions, updateUserRobotKnownPositions, }; diff --git a/frontend/src/views/FullScreenView.vue b/frontend/src/views/FullScreenView.vue index 1ac62084..818bc126 100644 --- a/frontend/src/views/FullScreenView.vue +++ b/frontend/src/views/FullScreenView.vue @@ -13,7 +13,7 @@
- +
@@ -49,15 +49,6 @@ onMounted(() => { lightMode.value = route.meta.lightMode == true; }); -const shownTiles = computed<{ data: Position[] } | undefined>(() => { - const positions = gameStore.globalKnownPositions; - - if (gameStore.currentGame.targetPosition !== undefined) { - positions.data.push(gameStore.currentGame.targetPosition); - } - return positions; -}); - const toggleLightMode = () => { lightMode.value = !lightMode.value; }; diff --git a/frontend/src/views/MainView.vue b/frontend/src/views/MainView.vue index 3b29dd4a..b567a8b3 100644 --- a/frontend/src/views/MainView.vue +++ b/frontend/src/views/MainView.vue @@ -2,7 +2,7 @@
- +
@@ -34,15 +34,6 @@ import WinnerBanner from "@/components/WinnerBanner.vue"; const gameStore = useGameStore(); const router = useRouter(); -const shownTiles = computed<{ data: Position[] } | undefined>(() => { - const positions = gameStore.globalKnownPositions; - - if (gameStore.currentGame.targetPosition !== undefined) { - positions.data.push(gameStore.currentGame.targetPosition); - } - return positions; -}); - const forwardToFullscreen = () => { router.push({ path: "/fullscreen" }); };