diff --git a/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/EndGameUseCase.kt b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/EndGameUseCase.kt new file mode 100644 index 00000000..2c1c8c9b --- /dev/null +++ b/application/src/main/kotlin/tw/waterballsa/gaas/application/usecases/EndGameUseCase.kt @@ -0,0 +1,36 @@ +package tw.waterballsa.gaas.application.usecases + +import tw.waterballsa.gaas.application.eventbus.EventBus +import tw.waterballsa.gaas.application.repositories.RoomRepository +import tw.waterballsa.gaas.application.repositories.UserRepository +import tw.waterballsa.gaas.domain.Room +import tw.waterballsa.gaas.events.EndedGameEvent +import tw.waterballsa.gaas.events.EndedGameEvent.Data +import tw.waterballsa.gaas.events.enums.EventMessageType.GAME_ENDED +import javax.inject.Named + +@Named +class EndGameUseCase( + roomRepository: RoomRepository, + userRepository: UserRepository, + private val eventBus: EventBus, +) : AbstractRoomUseCase(roomRepository, userRepository) { + fun execute(request: Request) { + val room = findRoomById(request.roomId) + room.endGame() + roomRepository.update(room) + + val endedGameEvent = room.endGameByGameService() + eventBus.broadcast(endedGameEvent) + } + + data class Request( + val roomId: String, + ) +} + +fun Room.endGameByGameService(): EndedGameEvent { + val type = GAME_ENDED + val data = Data(roomId!!.value) + return EndedGameEvent(type, data) +} \ No newline at end of file diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt index 8fa403d8..69aa1b4c 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/domain/Room.kt @@ -6,6 +6,7 @@ import tw.waterballsa.gaas.exceptions.PlatformException import tw.waterballsa.gaas.exceptions.enums.PlatformError.GAME_ALREADY_STARTED import tw.waterballsa.gaas.exceptions.enums.PlatformError.PLAYER_NOT_FOUND import tw.waterballsa.gaas.exceptions.enums.PlatformError.PLAYER_NOT_HOST +import tw.waterballsa.gaas.exceptions.enums.PlatformError.GAME_NOT_STARTED class Room( var roomId: Id? = null, @@ -33,6 +34,8 @@ class Room( fun isFull(): Boolean = players.size >= maxPlayers + fun isHost(playerId: Player.Id): Boolean = playerId == host.id + fun changePlayerReadiness(playerId: Player.Id, readiness: Boolean) { val player = findPlayer(playerId) ?: throw PlatformException(PLAYER_NOT_FOUND, "Player not joined") @@ -43,6 +46,14 @@ class Room( } } + fun endGame() { + if (status != PLAYING) { + throw PlatformException(GAME_NOT_STARTED, "Game has not started yet") + } + status = WAITING + cancelReadyForNonHostPlayers() + } + fun hasPlayer(playerId: Player.Id): Boolean = players.any { it.id == playerId } @@ -83,6 +94,14 @@ class Room( private fun findPlayer(playerId: Player.Id): Player? = players.find { it.id == playerId } + private fun cancelReadyForNonHostPlayers() { + players.forEach { player -> + if (!isHost(player.id)) { + player.cancelReady() + } + } + } + @JvmInline value class Id(val value: String) diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/events/EndedGameEvent.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/events/EndedGameEvent.kt new file mode 100644 index 00000000..26023966 --- /dev/null +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/events/EndedGameEvent.kt @@ -0,0 +1,12 @@ +package tw.waterballsa.gaas.events + +import tw.waterballsa.gaas.events.enums.EventMessageType + +data class EndedGameEvent( + val type: EventMessageType, + val data: Data, +) : DomainEvent() { + data class Data( + val roomId: String, + ) +} \ No newline at end of file diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/events/enums/EventMessageType.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/events/enums/EventMessageType.kt index 5e69f113..a9391c2b 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/events/enums/EventMessageType.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/events/enums/EventMessageType.kt @@ -11,4 +11,5 @@ enum class EventMessageType( USER_NOT_READY("USER_NOT_READY"), USER_JOINED("USER_JOINED"), USER_LEFT("USER_LEFT"), + GAME_ENDED("GAME_ENDED"), } diff --git a/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt b/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt index d43ee2ee..849fe744 100644 --- a/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt +++ b/domain/src/main/kotlin/tw/waterballsa/gaas/exceptions/enums/PlatformError.kt @@ -15,6 +15,7 @@ enum class PlatformError( GAME_CREATE_ERROR("G003"), GAME_ALREADY_STARTED("G004"), GAME_START_FAILED("G005"), + GAME_NOT_STARTED("G006"), USER_NOT_FOUND("U001"), USER_INPUT_INVALID("U002"), diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/configs/securities/SecurityConfig.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/configs/securities/SecurityConfig.kt index d6691c25..49a2abf8 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/configs/securities/SecurityConfig.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/configs/securities/SecurityConfig.kt @@ -34,6 +34,7 @@ class SecurityConfig( .antMatchers("/login", "/authenticate").permitAll() .antMatchers("/health", "/walking-skeleton").permitAll() .antMatchers("/swagger-ui/**", "/favicon.ico").permitAll() + .regexMatchers("/rooms/.*:endGame").permitAll() .anyRequest().authenticated() .and() .oauth2Login() diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt index bade1d02..ab37ef67 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/controllers/RoomController.kt @@ -28,6 +28,7 @@ class RoomController( private val getRoomUsecase: GetRoomUsecase, private val startGameUseCase: StartGameUseCase, private val fastJoinRoomUseCase: FastJoinRoomUseCase, + private val endGameUseCase: EndGameUseCase, ) { @PostMapping("/rooms") fun createRoom( @@ -150,6 +151,14 @@ class RoomController( return presenter.viewModel } + @PostMapping("/rooms/{roomId}:endGame") + @ResponseStatus(NO_CONTENT) + fun endGame( + @PathVariable roomId: String, + ) { + endGameUseCase.execute(EndGameUseCase.Request(roomId)) + } + class CreateRoomRequest( private val name: String, private val gameId: String, diff --git a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt index c84bb611..dacc15ef 100644 --- a/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt +++ b/spring/src/main/kotlin/tw/waterballsa/gaas/spring/repositories/SpringRoomRepositoryImpl.kt @@ -67,6 +67,7 @@ class SpringRoomRepository( minPlayers = minPlayers, name = name, password = password, + status = status ) private fun User.Id.toRoomPlayer(): Player = diff --git a/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt index ffcd11e7..14f5d314 100644 --- a/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt +++ b/spring/src/test/kotlin/tw/waterballsa/gaas/spring/it/controllers/RoomControllerTest.kt @@ -25,6 +25,9 @@ import tw.waterballsa.gaas.application.repositories.UserRepository import tw.waterballsa.gaas.domain.GameRegistration import tw.waterballsa.gaas.domain.Room import tw.waterballsa.gaas.domain.Room.Player +import tw.waterballsa.gaas.domain.Room.Status +import tw.waterballsa.gaas.domain.Room.Status.PLAYING +import tw.waterballsa.gaas.domain.Room.Status.WAITING import tw.waterballsa.gaas.domain.User import tw.waterballsa.gaas.exceptions.PlatformException import tw.waterballsa.gaas.exceptions.enums.PlatformError.GAME_START_FAILED @@ -40,7 +43,6 @@ import tw.waterballsa.gaas.spring.utils.Users.Companion.defaultUser import java.util.UUID.randomUUID import kotlin.reflect.KClass - class RoomControllerTest @Autowired constructor( val userRepository: UserRepository, val roomRepository: RoomRepository, @@ -411,6 +413,28 @@ class RoomControllerTest @Autowired constructor( .thenShouldFail("Player(${userB.id!!.value}) has joined another room.") } + @Test + fun givenHostAndPlayerBArePlayingInRoomC_WhenEndGame_ThenRoomCAndPlayersStatusAreChanged() { + val userA = testUser + val host = userA.toRoomPlayer() + val playerB = defaultUser("2").createUser().toRoomPlayer() + + givenPlayersArePlayingInRoom(host, playerB) + .wheEndGame() + .thenRoomAndPlayersStatusAreChanged() + } + + @Test + fun givenHostAndPlayerBAreWaitingInRoomC_WhenEndGame_ThenShouldFailed() { + val userA = testUser + val host = userA.toRoomPlayer() + val playerB = defaultUser("2").createUser().toRoomPlayer() + + givenHostAndPlayersJoinedTheRoom(host, playerB) + .wheEndGame() + .thenShouldFail("Game has not started yet") + } + private fun TestGetRoomsRequest.whenUserAVisitLobby(joinUser: User): ResultActions = mockMvc.perform( get("/rooms") @@ -493,6 +517,13 @@ class RoomControllerTest @Autowired constructor( return testRoom } + private fun givenPlayersArePlayingInRoom(host: Player, vararg players: Player): Room { + players.forEach { it.ready() } + val allPlayers = mutableListOf(host, *players) + testRoom = createRoom(host = host, players = allPlayers, status = PLAYING) + return testRoom + } + private fun Room.whenUserJoinTheRoom(user: User, password: String? = null): ResultActions = user.joinRoom(roomId!!.value, joinRoomRequest(password)) @@ -517,6 +548,11 @@ class RoomControllerTest @Autowired constructor( .withJwt(user.toJwt()) ) + private fun Room.wheEndGame(): ResultActions = + mockMvc.perform( + post("/rooms/${testRoom.roomId!!.value}:endGame") + ) + private fun ResultActions.thenCreateRoomSuccessfully() { val roomView = getBody(CreateRoomViewModel::class.java) val room = roomRepository.findById(roomView.id)!! @@ -655,19 +691,25 @@ class RoomControllerTest @Autowired constructor( ) ) - private fun createRoom(host: Player, players: MutableList, password: String? = null): Room = - roomRepository.createRoom( + private fun createRoom( + host: Player, + players: MutableList, + password: String? = null, + status: Status? = WAITING, + ): Room { + return roomRepository.createRoom( Room( game = testGame, - host = host, + host = Player(Player.Id(host.id!!.value), host.nickname, true), players = players, maxPlayers = testGame.maxPlayers, minPlayers = testGame.minPlayers, name = "My Room", - status = Room.Status.WAITING, + status = status!!, password = password ) ) + } private fun createRoomRequest(password: String? = null): TestCreateRoomRequest = TestCreateRoomRequest( @@ -754,4 +796,19 @@ class RoomControllerTest @Autowired constructor( andExpect(status().isBadRequest) .andExpect(jsonPath("$.message").value(message)) } + + private fun ResultActions.thenRoomAndPlayersStatusAreChanged() { + andExpect(status().isNoContent) + val room = roomRepository.findById(testRoom.roomId!!)!! + room.let { + assertEquals(WAITING, it.status) + assertFalse(it.isEmpty()) + assertTrue(it.host.readiness) + it.players.forEach { player -> + if (!it.isHost(player.id)) { + assertFalse(player.readiness) + } + } + } + } }