From eef32bd9307a3a9f1ee3b532db2fb1f7cf37927a Mon Sep 17 00:00:00 2001 From: joffrey-bion Date: Thu, 10 Dec 2020 23:06:00 +0100 Subject: Allow owner to leave/disband the game Resolves: https://github.com/joffrey-bion/seven-wonders/issues/51 --- .../server/controllers/GameBrowserController.kt | 11 +++--- .../server/controllers/LobbyController.kt | 41 ++++++++++++++++++---- .../org/luxons/sevenwonders/server/lobby/Lobby.kt | 4 +-- .../luxons/sevenwonders/server/SevenWondersTest.kt | 19 +++++----- .../controllers/GameBrowserControllerTest.kt | 11 +++--- .../server/controllers/LobbyControllerTest.kt | 40 +++++++++++++++------ 6 files changed, 84 insertions(+), 42 deletions(-) (limited to 'sw-server') diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt index 587afc10..d6573f8f 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt @@ -1,8 +1,11 @@ package org.luxons.sevenwonders.server.controllers +import org.luxons.sevenwonders.model.api.GameListEvent +import org.luxons.sevenwonders.model.api.GameListEventWrapper import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.model.api.actions.CreateGameAction import org.luxons.sevenwonders.model.api.actions.JoinGameAction +import org.luxons.sevenwonders.model.api.wrap import org.luxons.sevenwonders.server.ApiMisuseException import org.luxons.sevenwonders.server.api.toDTO import org.luxons.sevenwonders.server.lobby.Lobby @@ -36,9 +39,9 @@ class GameBrowserController( * @return the current list of [Lobby]s */ @SubscribeMapping("/games") // prefix /topic not shown - fun listGames(principal: Principal): List { + fun listGames(principal: Principal): GameListEventWrapper { logger.info("Player '{}' subscribed to /topic/games", principal.name) - return lobbyRepository.list().map { it.toDTO() } + return GameListEvent.ReplaceList(lobbyRepository.list().map { it.toDTO() }).wrap() } /** @@ -61,9 +64,7 @@ class GameBrowserController( // notify everyone that a new game exists val lobbyDto = lobby.toDTO() - // we need to pass a non-generic type (array is fine) so that Spring doesn't break when trying to find a - // serializer from Kotlinx Serialization - template.convertAndSend("/topic/games", listOf(lobbyDto).toTypedArray()) + template.convertAndSend("/topic/games", GameListEvent.CreateOrUpdate(lobbyDto).wrap()) return lobbyDto } diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt index 7757c9d9..6cc404e9 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt @@ -3,10 +3,12 @@ package org.luxons.sevenwonders.server.controllers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.luxons.sevenwonders.bot.SevenWondersBot +import org.luxons.sevenwonders.model.api.GameListEvent import org.luxons.sevenwonders.model.api.actions.AddBotAction import org.luxons.sevenwonders.model.api.actions.ReassignWondersAction import org.luxons.sevenwonders.model.api.actions.ReorderPlayersAction import org.luxons.sevenwonders.model.api.actions.UpdateSettingsAction +import org.luxons.sevenwonders.model.api.wrap import org.luxons.sevenwonders.model.hideHandsAndWaitForReadiness import org.luxons.sevenwonders.server.api.toDTO import org.luxons.sevenwonders.server.lobby.Lobby @@ -44,12 +46,38 @@ class LobbyController( val player = principal.player val lobby = player.lobby lobby.removePlayer(principal.name) + logger.info("Player {} left game '{}'", player, lobby.name) + template.convertAndSendToUser(player.username, "/queue/lobby/left", lobby.id) + if (lobby.getPlayers().isEmpty()) { - lobbyRepository.remove(lobby.id) + deleteLobby(lobby) + } else { + sendLobbyUpdateToPlayers(lobby) } + } - logger.info("Player {} left game '{}'", player, lobby.name) - sendLobbyUpdateToPlayers(lobby) + /** + * Disbands the current group, making everyone leave the lobby. + * + * @param principal the connected user's information + */ + @MessageMapping("/lobby/disband") + fun disband(principal: Principal) { + val player = principal.player + val lobby = player.ownedLobby + + lobby.getPlayers().forEach { + it.leave() + template.convertAndSendToUser(it.username, "/queue/lobby/left", lobby.id) + } + logger.info("Player {} disbanded game '{}'", player, lobby.name) + deleteLobby(lobby) + } + + private fun deleteLobby(lobby: Lobby) { + lobbyRepository.remove(lobby.id) + template.convertAndSend("/topic/games", GameListEvent.Delete(lobby.id).wrap()) + logger.info("Game '{}' removed", lobby.name) } /** @@ -110,12 +138,11 @@ class LobbyController( } internal fun sendLobbyUpdateToPlayers(lobby: Lobby) { + val lobbyDto = lobby.toDTO() lobby.getPlayers().forEach { - template.convertAndSendToUser(it.username, "/queue/lobby/updated", lobby.toDTO()) + template.convertAndSendToUser(it.username, "/queue/lobby/updated", lobbyDto) } - // we need to pass a non-generic type (array is fine) so that Spring doesn't break when trying to find a - // serializer from Kotlinx Serialization - template.convertAndSend("/topic/games", listOf(lobby.toDTO()).toTypedArray()) + template.convertAndSend("/topic/games", GameListEvent.CreateOrUpdate(lobbyDto).wrap()) } /** diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt index 6ebe39c1..4176c485 100644 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt @@ -101,7 +101,7 @@ class Lobby( @Synchronized fun removePlayer(username: String): Player { - val playerIndex = find(username) + val playerIndex = players.indexOfFirst { it.username == username } if (playerIndex < 0) { throw UnknownPlayerException(username) } @@ -115,8 +115,6 @@ class Lobby( return player } - private fun find(username: String): Int = players.indexOfFirst { it.username == username } - fun setEndOfGame() { state = State.FINISHED } diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt index db50609a..ccc2e548 100644 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt @@ -10,16 +10,14 @@ import org.junit.runner.RunWith import org.luxons.sevenwonders.client.SevenWondersClient import org.luxons.sevenwonders.client.SevenWondersSession import org.luxons.sevenwonders.client.joinGameAndWaitLobby +import org.luxons.sevenwonders.model.api.GameListEvent import org.luxons.sevenwonders.model.api.LobbyDTO import org.luxons.sevenwonders.server.test.runAsyncTest import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment import org.springframework.boot.web.server.LocalServerPort import org.springframework.test.context.junit4.SpringRunner -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.test.* @OptIn(FlowPreview::class) @RunWith(SpringRunner::class) @@ -94,18 +92,17 @@ class SevenWondersTest { val otherSession = newPlayer("OtherPlayer") val games = otherSession.watchGames().produceIn(this) - var receivedLobbies = withTimeout(500) { games.receive() } - assertNotNull(receivedLobbies) - assertEquals(0, receivedLobbies.size) + val initialListEvent = withTimeout(500) { games.receive() } + assertTrue(initialListEvent is GameListEvent.ReplaceList) + assertEquals(0, initialListEvent.lobbies.size) val ownerSession = newPlayer("GameOwner") val gameName = "Test Game" val createdLobby = ownerSession.createGameAndWaitLobby(gameName) - receivedLobbies = withTimeout(500) { games.receive() } - assertNotNull(receivedLobbies) - assertEquals(1, receivedLobbies.size) - val receivedLobby = receivedLobbies[0] + val afterGameListEvent = withTimeout(500) { games.receive() } + assertTrue(afterGameListEvent is GameListEvent.CreateOrUpdate) + val receivedLobby = afterGameListEvent.lobby assertEquals(createdLobby.id, receivedLobby.id) assertEquals(createdLobby.name, receivedLobby.name) diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt index 648c924b..df2e02ee 100644 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt @@ -2,6 +2,7 @@ package org.luxons.sevenwonders.server.controllers import org.junit.Before import org.junit.Test +import org.luxons.sevenwonders.model.api.GameListEvent import org.luxons.sevenwonders.model.api.actions.CreateGameAction import org.luxons.sevenwonders.model.api.actions.JoinGameAction import org.luxons.sevenwonders.server.controllers.GameBrowserController.UserAlreadyInGameException @@ -32,8 +33,8 @@ class GameBrowserControllerTest { @Test fun listGames_initiallyEmpty() { val principal = TestPrincipal("testuser") - val games = gameBrowserController.listGames(principal) - assertTrue(games.isEmpty()) + val gameListEvent = gameBrowserController.listGames(principal).event as GameListEvent.ReplaceList + assertTrue(gameListEvent.lobbies.isEmpty()) } @Test @@ -47,9 +48,9 @@ class GameBrowserControllerTest { assertEquals("Test Game", createdLobby.name) - val games = gameBrowserController.listGames(principal) - assertFalse(games.isEmpty()) - val lobby = games.iterator().next() + val gameListEvent = gameBrowserController.listGames(principal).event as GameListEvent.ReplaceList + assertFalse(gameListEvent.lobbies.isEmpty()) + val lobby = gameListEvent.lobbies.first() assertEquals(lobby, createdLobby) assertEquals(player.username, lobby.players[0].username) } diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt index 73a24ef0..097e4792 100644 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt @@ -14,12 +14,8 @@ import org.luxons.sevenwonders.server.repositories.LobbyRepository import org.luxons.sevenwonders.server.repositories.PlayerNotFoundException import org.luxons.sevenwonders.server.repositories.PlayerRepository import org.luxons.sevenwonders.server.test.mockSimpMessagingTemplate -import java.util.HashMap -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue +import java.util.* +import kotlin.test.* class LobbyControllerTest { @@ -69,7 +65,7 @@ class LobbyControllerTest { } @Test - fun leave_succeedsWhenInALobby_asOwner() { + fun leave_ownerAloneInLobby_succeedsAndRemovesLobby() { val player = playerRepository.createOrUpdate("testuser", "Test User") val lobby = lobbyRepository.create("Test Game", player) @@ -82,7 +78,29 @@ class LobbyControllerTest { } @Test - fun leave_succeedsWhenInALobby_asPeasant() { + fun leave_ownerInLobbyWithOthers_succeedsAndTransfersOwnership() { + val player1 = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player1) + val player2 = addPlayer(lobby, "testuser2") + + val principal = TestPrincipal("testuser") + lobbyController.leave(principal) + + assertTrue(lobbyRepository.list().contains(lobby)) + assertFalse(lobby.getPlayers().contains(player1)) + assertEquals(lobby.owner, player2) + + assertTrue(player2.isGameOwner) + assertTrue(player2.isInLobby) + assertFalse(player2.isInGame) + + assertFalse(player1.isGameOwner) + assertFalse(player1.isInLobby) + assertFalse(player1.isInGame) + } + + @Test + fun leave_succeedsWhenInALobby_asJoiner() { val player = playerRepository.createOrUpdate("testuser", "Test User") val lobby = lobbyRepository.create("Test Game", player) val player2 = addPlayer(lobby, "testuser2") @@ -118,7 +136,7 @@ class LobbyControllerTest { } @Test - fun reorderPlayers_failsForPeasant() { + fun reorderPlayers_failsForJoiner() { val player = playerRepository.createOrUpdate("testuser", "Test User") val lobby = lobbyRepository.create("Test Game", player) @@ -157,7 +175,7 @@ class LobbyControllerTest { } @Test - fun updateSettings_failsForPeasant() { + fun updateSettings_failsForJoiner() { val player = playerRepository.createOrUpdate("testuser", "Test User") val lobby = lobbyRepository.create("Test Game", player) @@ -189,7 +207,7 @@ class LobbyControllerTest { } @Test - fun startGame_failsForPeasant() { + fun startGame_failsForJoiner() { val player = playerRepository.createOrUpdate("testuser", "Test User") val lobby = lobbyRepository.create("Test Game", player) -- cgit