From d2d65836ab64f5d7555cf565b54642f754e6c9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 4 Aug 2026 22:51:12 +0200 Subject: [PATCH] [Server] Shuffle and reorder player seats before a game starts Auto-shuffles the seats at game start when enabled (now for games with two or more players instead of three or more) and handles the new SET_PLAYER_ORDER command so the game host or a moderator can reorder or randomize the seats before the game starts. Re-seating re-keys the player ids while keeping the per-user game/seat routing and persistent player records in sync, then broadcasts the new order to everyone. --- .../network/server/remote/game/game_config.h | 1 + .../game/server_abstract_participant.cpp | 31 ++ .../remote/game/server_abstract_participant.h | 7 + .../server/remote/game/server_game.cpp | 131 ++++++++- .../network/server/remote/game/server_game.h | 8 + .../server/remote/server_protocolhandler.cpp | 4 +- tests/movecard_tests/CMakeLists.txt | 12 + tests/movecard_tests/player_order_test.cpp | 269 ++++++++++++++++++ 8 files changed, 457 insertions(+), 6 deletions(-) create mode 100644 tests/movecard_tests/player_order_test.cpp diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h index baf946632..0661bd2aa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h @@ -21,6 +21,7 @@ struct GameConfig bool spectatorsSeeEverything = true; int startingLifeTotal = 20; bool shareDecklistsOnLoad = true; + bool shufflePlayers = false; }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index ff8ea14f3..5498d5b63 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -434,6 +435,33 @@ Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ return Response::RespOk; } +Response::ResponseCode Server_AbstractParticipant::cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if ((game->getHostId() != playerId) && !(userInfo->user_level() & ServerInfo_User::IsModerator)) { + return Response::RespFunctionNotAllowed; + } + + if (game->getGameStarted()) { + return Response::RespContextError; + } + + if (cmd.randomize()) { + game->shufflePlayerSeats(); + } else { + QList playerNames; + for (int i = 0; i < cmd.player_names_size(); ++i) { + playerNames.append(QString::fromStdString(cmd.player_names(i))); + } + game->reorderPlayerSeats(playerNames); + } + + game->sendGameStateToPlayers(); + + return Response::RespOk; +} + Response::ResponseCode Server_AbstractParticipant::processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges) { @@ -543,6 +571,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::SET_PLAYMAT: return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges); break; + case GameCommand::SET_PLAYER_ORDER: + return cmdSetPlayerOrder(command.GetExtension(Command_SetPlayerOrder::ext), rc, ges); + break; default: return Response::RespInvalidCommand; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index b6d4a8025..316b9727c 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -53,6 +53,7 @@ class Command_DeckSelect; class Command_SetSideboardLock; class Command_ChangeZoneProperties; class Command_SetPlaymat; +class Command_SetPlayerOrder; class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container { @@ -89,6 +90,10 @@ public: { return playerId; } + void setPlayerId(int _playerId) + { + playerId = _playerId; + } bool isSpectator() const { return roleInfo.isSpectator; @@ -173,6 +178,8 @@ public: virtual Response::ResponseCode cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); virtual Response::ResponseCode + cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 49db999f6..cc5220da5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -73,11 +73,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -91,8 +93,8 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword), spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything), startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad), - inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), - turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), + shufflePlayers(config.shufflePlayers), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), + firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), deckValidationStrategy(new Server_DefaultDeckValidationStrategy), lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy), gameMutex() @@ -309,13 +311,17 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, for (Server_AbstractParticipant *participant : participants.values()) { participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo); } + + if (recipient != nullptr) { + event->set_local_player_id(recipient->getPlayerId()); + } } void Server_Game::sendGameStateToPlayers() { // game state information for replay and omniscient spectators Event_GameStateChanged omniscientEvent; - createGameStateChangedEvent(&omniscientEvent, nullptr, true, false); + createGameStateChangedEvent(&omniscientEvent, nullptr, true, true); GameEventContainer *replayCont = prepareGameEvent(omniscientEvent, -1); replayCont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); @@ -327,7 +333,7 @@ void Server_Game::sendGameStateToPlayers() // the data we used for the replay. All spectators are equal, so we don't need to make a createGameStateChangedEvent // call for each one. Event_GameStateChanged spectatorNormalEvent; - createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, false); + createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, true); // send game state info to clients according to their role in the game for (auto *participant : participants.values()) { @@ -340,7 +346,7 @@ void Server_Game::sendGameStateToPlayers() } } else { Event_GameStateChanged event; - createGameStateChangedEvent(&event, participant, participant->isJudge(), false); + createGameStateChangedEvent(&event, participant, participant->isJudge(), true); gec = prepareGameEvent(event, -1); } @@ -380,6 +386,11 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) return; } + if (shufflePlayers && players.size() > 1) { + shufflePlayerSeats(); + players = getPlayers(); + } + for (Server_AbstractPlayer *player : players.values()) { player->setupZones(); } @@ -433,6 +444,115 @@ void Server_Game::startGameIfReady(bool forceStartGame) emit sigStartGameIfReady(forceStartGame); } +void Server_Game::shufflePlayerSeats() +{ + QMutexLocker locker(&gameMutex); + + // Split participants into players and spectators. Spectators keep their seats. + QList players; + QList spectators; + for (auto *participant : participants.values()) { + if (participant->isSpectator()) { + spectators.append(participant); + } else { + players.append(participant); + } + } + + // Fisher-Yates shuffle of the players. + QList shuffledPlayers; + while (!players.isEmpty()) { + const int index = rng->rand(0, players.size() - 1); + shuffledPlayers.append(players.takeAt(index)); + } + + reseatPlayers(shuffledPlayers + spectators, true); +} + +void Server_Game::reorderPlayerSeats(const QList &orderedNames) +{ + QMutexLocker locker(&gameMutex); + + // Split participants into players and spectators. Spectators keep their seats. + QList players; + QList spectators; + for (auto *participant : participants.values()) { + if (participant->isSpectator()) { + spectators.append(participant); + } else { + players.append(participant); + } + } + + // Order the players according to the given names, appending any player that + // was not listed (should not happen) in their current seat order. + QList orderedPlayers; + QSet placed; + for (const QString &name : orderedNames) { + for (auto *participant : players) { + if (!placed.contains(participant) && (QString::fromStdString(participant->getUserInfo()->name()) == name)) { + orderedPlayers.append(participant); + placed.insert(participant); + break; + } + } + } + for (auto *participant : players) { + if (!placed.contains(participant)) { + orderedPlayers.append(participant); + } + } + + reseatPlayers(orderedPlayers + spectators, false); +} + +void Server_Game::reseatPlayers(const QList &orderedParticipants, bool randomized) +{ + QMutexLocker locker(&gameMutex); + + // Reassign fresh, unique player ids following the given order. + QMap newParticipants; + int newHostId = -1; + for (auto *participant : orderedParticipants) { + const int newPlayerId = nextPlayerId++; + const int oldPlayerId = participant->getPlayerId(); + participant->setPlayerId(newPlayerId); + newParticipants.insert(newPlayerId, participant); + + // Keep the user interface's per-user game/seat mapping in sync so that + // incoming game commands are routed to the participant under its new id. + if (participant->getUserInterface()) { + participant->getUserInterface()->playerAddedToGame(gameId, room->getId(), newPlayerId); + } + + if (!participant->isSpectator() && (participant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered)) { + const QString playerName = QString::fromStdString(participant->getUserInfo()->name()); + room->getServer()->removePersistentPlayer(playerName, room->getId(), gameId, oldPlayerId); + room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newPlayerId); + } + + if (participant->getUserInfo()->name() == creatorInfo->name()) { + newHostId = newPlayerId; + } + } + participants = newParticipants; + + if (newHostId != -1) { + hostId = newHostId; + sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); + } + + // Broadcast the new seating order so that every client can display it. + Event_PlayerOrderChanged event; + for (auto *participant : orderedParticipants) { + if (!participant->isSpectator()) { + event.add_player_names(QString::fromStdString(participant->getUserInfo()->name()).toStdString()); + } + } + event.set_randomized(randomized); + sendGameEventContainer(prepareGameEvent(event, -1)); +} + void Server_Game::stopGameIfFinished() { QMutexLocker locker(&gameMutex); @@ -950,6 +1070,7 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_spectators_can_chat(spectatorsCanTalk); result.set_spectators_omniscient(spectatorsSeeEverything); result.set_share_decklists_on_load(shareDecklistsOnLoad); + result.set_shuffle_players(shufflePlayers); result.set_spectators_count(getSpectatorCount()); result.set_start_time(startTime.toSecsSinceEpoch()); } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 363c0ca94..edb910682 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -90,6 +90,7 @@ private: bool spectatorsSeeEverything; int startingLifeTotal; bool shareDecklistsOnLoad; + bool shufflePlayers; int inactivityCounter; int startTimeOfThisGame, secondsElapsed; bool firstGameStarted; @@ -109,6 +110,7 @@ private: Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo); + void reseatPlayers(const QList &orderedParticipants, bool randomized); void storeGameInformation(); bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const; signals: @@ -189,6 +191,10 @@ public: { return shareDecklistsOnLoad; } + bool getShufflePlayers() const + { + return shufflePlayers; + } Response::ResponseCode checkJoin(ServerInfo_User *user, const QString &_password, const RoleInfo &roleInfo, bool overrideRestrictions); bool containsUser(const QString &userName) const; @@ -202,6 +208,8 @@ public: bool kickParticipant(int playerId); void startGameIfReady(bool forceStartGame); void stopGameIfFinished(); + void shufflePlayerSeats(); + void reorderPlayerSeats(const QList &orderedNames); int getActivePlayer() const { return activePlayer; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index f9cadc9aa..78b9d955f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -1116,6 +1116,7 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20; bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false; + bool shufflePlayers = cmd.has_shuffle_players() ? cmd.shuffle_players() : false; const int gameId = databaseInterface->getNextGameId(); if (gameId == -1) { @@ -1137,7 +1138,8 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room .spectatorsCanTalk = cmd.spectators_can_talk(), .spectatorsSeeEverything = cmd.spectators_see_everything(), .startingLifeTotal = startingLifeTotal, - .shareDecklistsOnLoad = shareDecklistsOnLoad}; + .shareDecklistsOnLoad = shareDecklistsOnLoad, + .shufflePlayers = shufflePlayers}; auto *game = new Server_Game(config, room); diff --git a/tests/movecard_tests/CMakeLists.txt b/tests/movecard_tests/CMakeLists.txt index 68d8fc84c..35d7b44ed 100755 --- a/tests/movecard_tests/CMakeLists.txt +++ b/tests/movecard_tests/CMakeLists.txt @@ -9,3 +9,15 @@ cockatrice_add_test( LABELS server ) + +cockatrice_add_test( + NAME + player_order_test + SOURCES + player_order_test.cpp + LIBS + libcockatrice_network_server_remote + libcockatrice_rng + LABELS + server +) diff --git a/tests/movecard_tests/player_order_test.cpp b/tests/movecard_tests/player_order_test.cpp new file mode 100644 index 000000000..92ee380c3 --- /dev/null +++ b/tests/movecard_tests/player_order_test.cpp @@ -0,0 +1,269 @@ +#include "game/game_config.h" +#include "game/server_abstract_participant.h" +#include "game/server_game.h" +#include "server_abstractuserinterface.h" +#include "server_response_containers.h" +#include "server_room.h" +#include "server_test_helpers.h" + +#include +#include +#include +#include +#include +#include +#include + +RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server + +class ScriptedRng : public RNG_Abstract +{ +public: + QList values; + + unsigned int rand(int min, int max) override + { + const unsigned int value = values.takeFirst(); + EXPECT_GE(value, static_cast(min)); + EXPECT_LE(value, static_cast(max)); + return value; + } +}; + +class FakeUserInterface : public Server_AbstractUserInterface +{ +public: + FakeUserInterface(Server *_server, const ServerInfo_User &_userInfo) : Server_AbstractUserInterface(_server) + { + setUserInfo(_userInfo); + } + + int getLastCommandTime() const override + { + return 0; + } + + bool addSaidMessageSize(int) override + { + return true; + } + + void sendProtocolItem(const Response &) override + { + } + + void sendProtocolItem(const SessionEvent &) override + { + } + + void sendProtocolItem(const GameEventContainer &) override + { + } + + void sendProtocolItem(const RoomEvent &) override + { + } +}; + +namespace +{ +QStringList participantNamesInOrder(const Server_Game &game) +{ + QStringList names; + for (const auto *participant : game.getParticipants()) { + names.append(QString::fromStdString(participant->getUserInfo()->name())); + } + return names; +} + +QList participantIdsInOrder(const Server_Game &game) +{ + QList ids; + for (int id : game.getParticipants().keys()) { + ids.append(id); + } + return ids; +} +} // namespace + +class PlayerOrderTest : public ::testing::Test +{ +protected: + ServerInfo_User creator; + ServerInfo_User userB; + ServerInfo_User userC; + + PlayerOrderTest() + { + creator.set_name("A"); + creator.set_user_level(ServerInfo_User::IsRegistered); + userB.set_name("B"); + userB.set_user_level(ServerInfo_User::IsRegistered); + userC.set_name("C"); + userC.set_user_level(ServerInfo_User::IsRegistered); + } +}; + +TEST_F(PlayerOrderTest, JoinOrderIsSeatOrder) +{ + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + FakeUserInterface uiA(&server, creator); + FakeUserInterface uiB(&server, userB); + FakeUserInterface uiC(&server, userC); + GameConfig config{.creatorInfo = creator, + .gameId = 1, + .description = QString(), + .password = QString(), + .maxPlayers = 3, + .gameTypes = QList(), + .onlyBuddies = false, + .onlyRegistered = false, + .spectatorsAllowed = false, + .spectatorsNeedPassword = false, + .spectatorsCanTalk = false, + .spectatorsSeeEverything = false, + .startingLifeTotal = 20, + .shareDecklistsOnLoad = false, + .shufflePlayers = false}; + Server_Game game(config, &room); + ResponseContainer rc(0); + + game.addPlayer(&uiA, rc, false, false); + game.addPlayer(&uiB, rc, false, false); + game.addPlayer(&uiC, rc, false, false); + + EXPECT_EQ(participantNamesInOrder(game), QStringList({"A", "B", "C"})); + EXPECT_EQ(participantIdsInOrder(game), QList({0, 1, 2})); + EXPECT_EQ(game.getHostId(), 0); + EXPECT_EQ(game.getPlayerCount(), 3); +} + +TEST_F(PlayerOrderTest, ReorderPlayerSeats) +{ + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + FakeUserInterface uiA(&server, creator); + FakeUserInterface uiB(&server, userB); + FakeUserInterface uiC(&server, userC); + GameConfig config{.creatorInfo = creator, + .gameId = 1, + .description = QString(), + .password = QString(), + .maxPlayers = 3, + .gameTypes = QList(), + .onlyBuddies = false, + .onlyRegistered = false, + .spectatorsAllowed = false, + .spectatorsNeedPassword = false, + .spectatorsCanTalk = false, + .spectatorsSeeEverything = false, + .startingLifeTotal = 20, + .shareDecklistsOnLoad = false, + .shufflePlayers = false}; + Server_Game game(config, &room); + ResponseContainer rc(0); + + game.addPlayer(&uiA, rc, false, false); + game.addPlayer(&uiB, rc, false, false); + game.addPlayer(&uiC, rc, false, false); + + game.reorderPlayerSeats({"C", "A", "B"}); + + const QList ids = participantIdsInOrder(game); + EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"})); + EXPECT_EQ(ids.size(), 3); + EXPECT_GE(ids.at(0), 3); // re-seated players get fresh ids + EXPECT_LT(ids.at(0), ids.at(1)); + EXPECT_LT(ids.at(1), ids.at(2)); + + // the host seat follows the creator + EXPECT_EQ(game.getHostId(), ids.at(1)); + + // every user interface's game list is updated to the new player id + const int gameId = game.getGameId(); + EXPECT_EQ(uiA.getGames().value(gameId).second, ids.at(1)); + EXPECT_EQ(uiB.getGames().value(gameId).second, ids.at(2)); + EXPECT_EQ(uiC.getGames().value(gameId).second, ids.at(0)); +} + +TEST_F(PlayerOrderTest, ReorderPlayerSeatsKeepsPlayerCount) +{ + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + FakeUserInterface uiA(&server, creator); + FakeUserInterface uiB(&server, userB); + FakeUserInterface uiC(&server, userC); + GameConfig config{.creatorInfo = creator, + .gameId = 1, + .description = QString(), + .password = QString(), + .maxPlayers = 3, + .gameTypes = QList(), + .onlyBuddies = false, + .onlyRegistered = false, + .spectatorsAllowed = false, + .spectatorsNeedPassword = false, + .spectatorsCanTalk = false, + .spectatorsSeeEverything = false, + .startingLifeTotal = 20, + .shareDecklistsOnLoad = false, + .shufflePlayers = false}; + Server_Game game(config, &room); + ResponseContainer rc(0); + + game.addPlayer(&uiA, rc, false, false); + game.addPlayer(&uiB, rc, false, false); + game.addPlayer(&uiC, rc, false, false); + + // listing only a subset appends the remaining players in their current order + game.reorderPlayerSeats({"C"}); + + EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"})); + EXPECT_EQ(game.getPlayerCount(), 3); + EXPECT_EQ(game.getHostId(), participantIdsInOrder(game).at(1)); +} + +TEST_F(PlayerOrderTest, ShufflePlayerSeatsIsAPermutation) +{ + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + FakeUserInterface uiA(&server, creator); + FakeUserInterface uiB(&server, userB); + FakeUserInterface uiC(&server, userC); + GameConfig config{.creatorInfo = creator, + .gameId = 1, + .description = QString(), + .password = QString(), + .maxPlayers = 3, + .gameTypes = QList(), + .onlyBuddies = false, + .onlyRegistered = false, + .spectatorsAllowed = false, + .spectatorsNeedPassword = false, + .spectatorsCanTalk = false, + .spectatorsSeeEverything = false, + .startingLifeTotal = 20, + .shareDecklistsOnLoad = false, + .shufflePlayers = false}; + Server_Game game(config, &room); + ResponseContainer rc(0); + + game.addPlayer(&uiA, rc, false, false); + game.addPlayer(&uiB, rc, false, false); + game.addPlayer(&uiC, rc, false, false); + + // fisher-yates on [A, B, C]: rand(0, 2) -> 1 (take B), rand(0, 1) -> 0 (take A), rand(0, 0) -> 0 (take C) + auto *scriptedRng = new ScriptedRng; + scriptedRng->values = {1, 0, 0}; + rng = scriptedRng; + + game.shufflePlayerSeats(); + + EXPECT_EQ(participantNamesInOrder(game), QStringList({"B", "A", "C"})); + const QList ids = participantIdsInOrder(game); + EXPECT_EQ(ids.size(), 3); + QSet idSet(ids.cbegin(), ids.cend()); + EXPECT_EQ(idSet.size(), 3); // ids are unique + EXPECT_EQ(game.getHostId(), ids.at(1)); +}