From 078d0858a352c2a135586add7cdff75f4f6d778c Mon Sep 17 00:00:00 2001 From: Bryce! Date: Sun, 8 Nov 2026 10:41:14 -0500 Subject: [PATCH] [Game] Reveal multiple distinct random hand cards to a recipient (#7753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add distinct random hand reveals with private recipient delivery * Route random hand reveals through PlayerActions Keep command creation, dispatch and response handling in the game action layer. HandMenu requests the typed operation and presents the count and failure dialogs. Show the chosen recipient, distinguish the count action from the immediate single-card reveal, and use the game window as parent. * Apply project formatting to random-reveal warning construction * Simplify random reveal to follow existing command patterns Forward server dispatch through cmdRevealRandomCards and use ordinary PlayerActions command sending. Remove the bespoke server-version error UI and long-lived-object guards. Reduce the test from 246 to 150 lines with two focused cases using the current RoleInfo player API, preserving distinct-card, private-event and rejected-count coverage. * Extract the shared reveal helper and name the test's CardRef fields RickyRister asked for cmdRevealCards and cmdRevealRandomCards to share a helper instead of threading a randomCount parameter through the reveal virtual. Restore the three-argument cmdRevealCards signature, resolve the reveal preconditions and zone in resolveRevealZone, and let both commands build their card list and hand it to revealCards, which owns the private and public Event_RevealCards construction and the enqueue. The random command no longer fabricates a Command_RevealCards with the -2 sentinel to re-enter the legacy path; its behaviour is unchanged. Name the CardRef fields in random_reveal_test instead of relying on declaration order, and state in the user documentation that a rejected reveal is not reported in the client, matching what the client does. --------- Co-authored-by: Lukas BrĂ¼bach <44814898+BruebachL@users.noreply.github.com> --- .../game_graphics/player/menu/hand_menu.cpp | 62 +++++++ .../src/game_graphics/player/menu/hand_menu.h | 6 + .../gameplay/revealing_cards.md | 11 ++ .../doxygen/pages/user_documentation/index.md | 4 + .../game/player/player_actions.cpp | 11 ++ .../game/player/player_actions.h | 1 + .../game/server_abstract_participant.cpp | 10 ++ .../remote/game/server_abstract_participant.h | 3 + .../remote/game/server_abstract_player.cpp | 93 +++++++++-- .../remote/game/server_abstract_player.h | 14 ++ .../protocol/pb/command_reveal_cards.proto | 10 ++ .../protocol/pb/game_commands.proto | 5 + tests/CMakeLists.txt | 11 ++ tests/random_reveal_test.cpp | 151 ++++++++++++++++++ 14 files changed, 375 insertions(+), 17 deletions(-) create mode 100644 docs/doxygen/pages/user_documentation/gameplay/revealing_cards.md create mode 100644 tests/random_reveal_test.cpp diff --git a/cockatrice/src/game_graphics/player/menu/hand_menu.cpp b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp index e992f3ff5..8ef47d4cb 100644 --- a/cockatrice/src/game_graphics/player/menu/hand_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp @@ -6,6 +6,9 @@ #include "../player_graphics_item.h" #include +#include +#include +#include #include #include #include @@ -67,6 +70,15 @@ HandMenu::HandMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(p aRevealRandomHandCardToAll->setData(-1); connect(aRevealRandomHandCardToAll, &QAction::triggered, this, &HandMenu::onRevealRandomHandCardTriggered); + mRevealRandomHandCards = addMenu(QString()); + mRevealRandomHandCards->setObjectName("revealRandomHandCardsMenu"); + connect(mRevealRandomHandCards, &QMenu::aboutToShow, this, + &HandMenu::populateRevealRandomHandCardsMenuWithActivePlayers); + aRevealRandomHandCardsToAll = new QAction(this); + aRevealRandomHandCardsToAll->setData(-1); + connect(aRevealRandomHandCardsToAll, &QAction::triggered, this, &HandMenu::onRevealRandomHandCardsTriggered); + mRevealRandomHandCards->addAction(aRevealRandomHandCardsToAll); + // We still need to add these actions to menu here so that the shortcuts are active right away mRevealHand->addAction(aRevealHandToAll); mRevealRandomHandCard->addAction(aRevealRandomHandCardToAll); @@ -146,6 +158,8 @@ void HandMenu::retranslateUi() mRevealRandomHandCard->setTitle(tr("Reveal r&andom card to...")); aRevealRandomHandCardToAll->setText(tr("All players")); + mRevealRandomHandCards->setTitle(tr("Reveal a chosen number of random cards to...")); + aRevealRandomHandCardsToAll->setText(tr("All players")); } } @@ -233,3 +247,51 @@ void HandMenu::onRevealRandomHandCardTriggered() const int targetId = action->data().toInt(); player->getLogic()->getPlayerActions()->actRevealRandomHandCard(targetId); } + +void HandMenu::populateRevealRandomHandCardsMenuWithActivePlayers() +{ + const bool hasCards = !player->getLogic()->getHandZone()->getCards().isEmpty(); + mRevealRandomHandCards->clear(); + aRevealRandomHandCardsToAll->setEnabled(hasCards); + mRevealRandomHandCards->addAction(aRevealRandomHandCardsToAll); + mRevealRandomHandCards->addSeparator(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); + for (auto *other : players) { + if (other == player->getLogic()) { + continue; + } + auto *action = mRevealRandomHandCards->addAction(other->getPlayerInfo()->getName()); + action->setData(other->getPlayerInfo()->getId()); + action->setEnabled(hasCards); + connect(action, &QAction::triggered, this, &HandMenu::onRevealRandomHandCardsTriggered); + } +} + +void HandMenu::onRevealRandomHandCardsTriggered() +{ + const auto *action = qobject_cast(sender()); + const int handSize = player->getLogic()->getHandZone()->getCards().size(); + if (!action || handSize == 0) { + return; + } + const int targetId = action->data().toInt(); + auto *actions = player->getLogic()->getPlayerActions(); + auto *dialog = new QInputDialog(dialogParent()); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle(tr("Reveal Random Hand Cards")); + dialog->setLabelText(tr("Number of distinct random cards to reveal to %1:").arg(action->text())); + dialog->setInputMode(QInputDialog::IntInput); + dialog->setIntRange(1, handSize); + dialog->setIntValue(qMin(2, handSize)); + connect(dialog, &QDialog::accepted, actions, + [dialog, actions, targetId] { actions->actRevealRandomHandCards(targetId, dialog->intValue()); }); + dialog->open(); +} + +QWidget *HandMenu::dialogParent() const +{ + if (auto *view = player->scene() ? player->scene()->views().value(0) : nullptr) { + return view->window(); + } + return nullptr; +} diff --git a/cockatrice/src/game_graphics/player/menu/hand_menu.h b/cockatrice/src/game_graphics/player/menu/hand_menu.h index de7726064..03ecd7544 100644 --- a/cockatrice/src/game_graphics/player/menu/hand_menu.h +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.h @@ -42,8 +42,11 @@ private slots: void populateRevealRandomHandCardMenuWithActivePlayers(); void onRevealHandTriggered(); void onRevealRandomHandCardTriggered(); + void populateRevealRandomHandCardsMenuWithActivePlayers(); + void onRevealRandomHandCardsTriggered(); private: + QWidget *dialogParent() const; PlayerGraphicsItem *player; QAction *aViewHand = nullptr; @@ -62,6 +65,9 @@ private: QMenu *mRevealRandomHandCard = nullptr; QAction *aRevealRandomHandCardToAll = nullptr; + QMenu *mRevealRandomHandCards = nullptr; + QAction *aRevealRandomHandCardsToAll = nullptr; + QMenu *mMoveHandMenu = nullptr; QAction *aMoveHandToTopLibrary = nullptr; QAction *aMoveHandToBottomLibrary = nullptr; diff --git a/docs/doxygen/pages/user_documentation/gameplay/revealing_cards.md b/docs/doxygen/pages/user_documentation/gameplay/revealing_cards.md new file mode 100644 index 000000000..5d47bd3b4 --- /dev/null +++ b/docs/doxygen/pages/user_documentation/gameplay/revealing_cards.md @@ -0,0 +1,11 @@ +@page revealing_cards Revealing Cards + +The **Hand** menu offers **Reveal a chosen number of random cards to...**. Choose **All players** or a specific player, then choose how many cards to reveal. The count dialog displays the chosen recipient. + +The server chooses distinct cards without replacement. Two copies of the same card are separate physical cards and can both be chosen. The operation does not reorder, move, or otherwise change the hand. + +When you choose a specific player, that player receives the selected card identities. Judges and spectators configured to see all hidden information retain their usual access. Ordinary participants other than the chosen player, including the hand's owner, receive a notice with the count and recipient, but do not learn which cards were chosen. Revealing to all players sends the selected identities to everyone, using the ordinary reveal window. + +If the hand becomes too small or the recipient leaves before the server processes the request, the server rejects the request and no cards are revealed. Cancelling the count dialog sends nothing. + +This operation requires a server that supports multiple random reveals. An older server rejects it without revealing any cards. Neither rejection is reported in the client; the reveal simply does not happen. The existing **Reveal random card to...** action and its shortcut remain available for single-card reveals. diff --git a/docs/doxygen/pages/user_documentation/index.md b/docs/doxygen/pages/user_documentation/index.md index f4ecbe8a1..e95706727 100644 --- a/docs/doxygen/pages/user_documentation/index.md +++ b/docs/doxygen/pages/user_documentation/index.md @@ -7,6 +7,10 @@ - @subpage editing_decks - @subpage exporting_decks +## Gameplay + +- @subpage revealing_cards + ## Release Channels - @subpage beta_release diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp index 90823fec6..1e854cbd4 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp +++ b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp @@ -1756,6 +1756,17 @@ void PlayerActions::actRevealRandomHandCard(int revealToPlayerId) sendGameCommand(cmd); } +void PlayerActions::actRevealRandomHandCards(int revealToPlayerId, int amount) +{ + Command_RevealRandomCards cmd; + if (revealToPlayerId != -1) { + cmd.set_player_id(revealToPlayerId); + } + cmd.set_zone_name(ZoneNames::HAND); + cmd.set_number_of_cards(amount); + sendGameCommand(cmd); +} + void PlayerActions::actRevealLibrary(int revealToPlayerId) { Command_RevealCards cmd; diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.h b/libcockatrice_game/libcockatrice/game/player/player_actions.h index 7ed52b9a1..81d18315c 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_actions.h +++ b/libcockatrice_game/libcockatrice/game/player/player_actions.h @@ -279,6 +279,7 @@ public slots: void actReveal(QList selectedCards, QAction *action); void actRevealHand(int revealToPlayerId); void actRevealRandomHandCard(int revealToPlayerId); + void actRevealRandomHandCards(int revealToPlayerId, int amount); void actRevealLibrary(int revealToPlayerId); void actSortHand(); 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 dcd14d14c..f7739bc85 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 @@ -405,6 +405,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdRevealCards(const Command_ return Response::RespFunctionNotAllowed; } +Response::ResponseCode Server_AbstractParticipant::cmdRevealRandomCards(const Command_RevealRandomCards & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + Response::ResponseCode Server_AbstractParticipant::cmdChangeZoneProperties(const Command_ChangeZoneProperties & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) @@ -544,6 +551,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::REVEAL_CARDS: return cmdRevealCards(command.GetExtension(Command_RevealCards::ext), rc, ges); break; + case GameCommand::REVEAL_RANDOM_CARDS: + return cmdRevealRandomCards(command.GetExtension(Command_RevealRandomCards::ext), rc, ges); + break; case GameCommand::MOVE_CARD: return cmdMoveCard(command.GetExtension(Command_MoveCard::ext), rc, ges); break; 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 316b9727c..6b2a93e48 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 @@ -46,6 +46,7 @@ class Command_NextTurn; class Command_SetActivePhase; class Command_DumpZone; class Command_RevealCards; +class Command_RevealRandomCards; class Command_ReverseTurn; class Command_MoveCard; class Command_SetSideboardPlan; @@ -176,6 +177,8 @@ public: virtual Response::ResponseCode cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges); virtual Response::ResponseCode + cmdRevealRandomCards(const Command_RevealRandomCards &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); virtual Response::ResponseCode cmdSetPlayerOrder(const Command_SetPlayerOrder &cmd, ResponseContainer &rc, GameEventStorage &ges); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index b5a620798..b9617b547 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -1494,8 +1494,10 @@ Server_AbstractPlayer::cmdDumpZone(const Command_DumpZone &cmd, ResponseContaine return Response::RespOk; } -Response::ResponseCode -Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +Response::ResponseCode Server_AbstractPlayer::resolveRevealZone(const std::string &zoneName, + bool hasRecipient, + int recipientId, + Server_CardZone *&zone) { if (!game->getGameStarted()) { return Response::RespGameNotStarted; @@ -1504,17 +1506,61 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo return Response::RespContextError; } - if (cmd.has_player_id()) { - Server_AbstractPlayer *otherPlayer = game->getPlayer(cmd.player_id()); - if (!otherPlayer) { - return Response::RespNameNotFound; - } + if (hasRecipient && !game->getPlayer(recipientId)) { + return Response::RespNameNotFound; } - Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + + zone = zones.value(nameFromStdString(zoneName)); if (!zone) { return Response::RespNameNotFound; } + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdRevealRandomCards(const Command_RevealRandomCards &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (cmd.number_of_cards() <= 0) { + return Response::RespInvalidData; + } + + Server_CardZone *zone = nullptr; + const Response::ResponseCode result = + resolveRevealZone(cmd.zone_name(), cmd.has_player_id(), cmd.player_id(), zone); + if (result != Response::RespOk) { + return result; + } + + auto candidates = zone->getCards(); + if (cmd.number_of_cards() > candidates.size()) { + return Response::RespContextError; + } + + // Partial Fisher-Yates on a copy: each physical card can appear only once, + // and the hand's order and contents stay unchanged. + QList cardsToReveal; + for (int i = 0; i < cmd.number_of_cards(); ++i) { + const int selected = rng->rand(i, candidates.size() - 1); + candidates.swapItemsAt(i, selected); + cardsToReveal.append(candidates.at(i)); + } + + // -2 in the public event tells other participants only how many cards were revealed. + return revealCards(zone, cardsToReveal, {-2}, cmd.has_player_id(), cmd.player_id(), false, ges); +} + +Response::ResponseCode +Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + Server_CardZone *zone = nullptr; + const Response::ResponseCode result = + resolveRevealZone(cmd.zone_name(), cmd.has_player_id(), cmd.player_id(), zone); + if (result != Response::RespOk) { + return result; + } + QList cardsToReveal; if (cmd.top_cards() != -1) { for (int i = 0; i < cmd.top_cards(); i++) { @@ -1547,15 +1593,28 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo } } + const QList cardIdsForOthers(cmd.card_id().begin(), cmd.card_id().end()); + return revealCards(zone, cardsToReveal, cardIdsForOthers, cmd.has_player_id(), cmd.player_id(), + cmd.grant_write_access(), ges); +} + +Response::ResponseCode Server_AbstractPlayer::revealCards(Server_CardZone *zone, + const QList &cardsToReveal, + const QList &cardIdsForOthers, + bool hasRecipient, + int recipientId, + bool grantWriteAccess, + GameEventStorage &ges) +{ Event_RevealCards eventOthers; - eventOthers.set_grant_write_access(cmd.grant_write_access()); + eventOthers.set_grant_write_access(grantWriteAccess); eventOthers.set_zone_name(zone->getName().toStdString()); eventOthers.set_number_of_cards(cardsToReveal.size()); - for (auto cardId : cmd.card_id()) { + for (int cardId : cardIdsForOthers) { eventOthers.add_card_id(cardId); } - if (cmd.has_player_id()) { - eventOthers.set_other_player_id(cmd.player_id()); + if (hasRecipient) { + eventOthers.set_other_player_id(recipientId); } Event_RevealCards eventPrivate(eventOthers); @@ -1592,19 +1651,19 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo } } - if (cmd.has_player_id()) { - if (cmd.grant_write_access()) { - zone->addWritePermission(cmd.player_id()); + if (hasRecipient) { + if (grantWriteAccess) { + zone->addWritePermission(recipientId); } if (isJudge()) { ges.setOverwriteOwnership(true); } - ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, cmd.player_id()); + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, recipientId); ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); } else { - if (cmd.grant_write_access()) { + if (grantWriteAccess) { const QList &participantIds = game->getParticipants().keys(); for (int anyParticipantId : participantIds) { zone->addWritePermission(anyParticipantId); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index ae20955bf..c92c4c3dd 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -32,6 +32,18 @@ private: void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges); void getPlayerProperties(ServerInfo_PlayerProperties &result) override; + /// Validates the preconditions shared by every reveal and resolves the source zone. + Response::ResponseCode + resolveRevealZone(const std::string &zoneName, bool hasRecipient, int recipientId, Server_CardZone *&zone); + /// Builds the private and public reveal events for an already chosen set of cards and enqueues them. + Response::ResponseCode revealCards(Server_CardZone *zone, + const QList &cardsToReveal, + const QList &cardIdsForOthers, + bool hasRecipient, + int recipientId, + bool grantWriteAccess, + GameEventStorage &ges); + protected: bool conceded; DeckList *deck; @@ -163,6 +175,8 @@ public: cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) override; virtual Response::ResponseCode cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdRevealRandomCards(const Command_RevealRandomCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges) override; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto index 82216911c..6bca5a69e 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto @@ -10,3 +10,13 @@ message Command_RevealCards { optional bool grant_write_access = 4; optional sint32 top_cards = 5 [default = -1]; } + +// Separate extension so older servers reject the operation without revealing any cards. +message Command_RevealRandomCards { + extend GameCommand { + optional Command_RevealRandomCards ext = 1036; + } + optional string zone_name = 1; + optional sint32 number_of_cards = 2; + optional sint32 player_id = 3 [default = -1]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index eb399ffb9..10ceb33ba 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -185,6 +185,11 @@ message GameCommand { /// Server: Server_AbstractParticipant::cmdSetPlayerOrder /// Client: reflected via Event_PlayerOrderChanged SET_PLAYER_ORDER = 1206; + + /// Reveal a distinct random sample without exposing identities to other recipients. + /// Server: Server_AbstractPlayer::cmdRevealCards + /// Client: PlayerEventHandler::eventRevealCards + REVEAL_RANDOM_CARDS = 1036; } extensions 100 to max; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 62523f456..04b3575aa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -356,6 +356,17 @@ cockatrice_add_test( LABELS server ) +cockatrice_add_test( + NAME + random_reveal_test + SOURCES + random_reveal_test.cpp + LIBS + libcockatrice_network_server_remote + libcockatrice_rng + LABELS + server +) cockatrice_add_test( NAME server_game_join_test diff --git a/tests/random_reveal_test.cpp b/tests/random_reveal_test.cpp new file mode 100644 index 000000000..62defe183 --- /dev/null +++ b/tests/random_reveal_test.cpp @@ -0,0 +1,151 @@ +#include "game/game_config.h" +#include "game/server_abstract_player.h" +#include "game/server_card.h" +#include "game/server_cardzone.h" +#include "game/server_game.h" +#include "movecard_tests/server_test_helpers.h" +#include "server_abstractuserinterface.h" +#include "server_response_containers.h" +#include "server_room.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +class FirstRng : public RNG_Abstract +{ + unsigned int rand(int min, int) override + { + return min; + } +} firstRng; + +class User : public Server_AbstractUserInterface +{ +public: + User(Server *server, const char *name) : Server_AbstractUserInterface(server) + { + ServerInfo_User info; + info.set_name(name); + setUserInfo(info); + } + 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 RoomEvent &) override + { + } + void sendProtocolItem(const GameEventContainer &) override + { + } +}; + +class RandomReveal : public ::testing::Test +{ +protected: + FakeServer server; + User owner{&server, "Owner"}, target{&server, "Target"}; + Server_Room room{0, 0, "", "", "", "", false, "", {}, &server}; + std::unique_ptr game; + Server_AbstractPlayer *player; + Server_CardZone *hand; + ResponseContainer response{1}; + + void SetUp() override + { + GameConfig config; + config.creatorInfo = *owner.getUserInfo(); + config.gameId = 1; + config.maxPlayers = 2; + config.shareDecklistsOnLoad = false; + game = std::make_unique(config, &room); + game->addPlayer(&owner, response, {}); + game->addPlayer(&target, response, {}); + for (auto *participant : game->getPlayers()) { + GameCommand command; + command.MutableExtension(Command_DeckSelect::ext) + ->set_deck(DeckList().writeToString_Native().toStdString()); + GameEventStorage events; + ASSERT_EQ(participant->processGameCommand(command, response, events), Response::RespOk); + participant->setReadyStart(true); + } + game->startGameIfReady(false); + QCoreApplication::processEvents(); + ASSERT_TRUE(game->getGameStarted()); + player = game->getPlayer(0); + hand = player->getZones().value(ZoneNames::HAND); + for (int i = 0; i < 4; ++i) { + hand->insertCard( + new Server_Card({.name = "Twin", .providerId = QString::number(i)}, player->newCardId(), i, 0), -1, 0); + } + } + + Response::ResponseCode reveal(int count, GameEventStorage &events) + { + GameCommand command; + auto *request = command.MutableExtension(Command_RevealRandomCards::ext); + request->set_zone_name(ZoneNames::HAND); + request->set_player_id(1); + request->set_number_of_cards(count); + return player->processGameCommand(command, response, events); + } +}; + +TEST_F(RandomReveal, DistinctPhysicalCardsArePrivateAndLeaveTheHandUnchanged) +{ + const auto original = hand->getCards(); + GameEventStorage events; + ASSERT_EQ(reveal(2, events), Response::RespOk); + ASSERT_EQ(events.getGameEventList().size(), 2); + EXPECT_EQ(events.getPrivatePlayerId(), 1); + const auto *privateEvent = events.getGameEventList().at(0); + EXPECT_EQ(privateEvent->getRecipients(), GameEventStorageItem::SendToPrivate); + const auto &cards = privateEvent->getGameEvent().GetExtension(Event_RevealCards::ext); + ASSERT_EQ(cards.cards_size(), 2); + EXPECT_NE(cards.cards(0).id(), cards.cards(1).id()); + const auto *publicEvent = events.getGameEventList().at(1); + EXPECT_EQ(publicEvent->getRecipients(), GameEventStorageItem::SendToOthers); + const auto ¬ice = publicEvent->getGameEvent().GetExtension(Event_RevealCards::ext); + EXPECT_EQ(notice.cards_size(), 0); + EXPECT_EQ(notice.number_of_cards(), 2); + EXPECT_EQ(hand->getCards(), original); +} + +TEST_F(RandomReveal, InvalidCountsDoNotQueueAnyReveal) +{ + for (int count : {0, -1, 5}) { + GameEventStorage events; + EXPECT_NE(reveal(count, events), Response::RespOk); + EXPECT_TRUE(events.getGameEventList().isEmpty()); + } +} +} // namespace + +RNG_Abstract *rng = &firstRng; + +int main(int argc, char **argv) +{ + QCoreApplication application(argc, argv); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}