mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Merge 6fab4410d0 into df74917cb0
This commit is contained in:
commit
653521cebf
14 changed files with 450 additions and 8 deletions
|
|
@ -6,11 +6,16 @@
|
|||
#include "../player_graphics_item.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsView>
|
||||
#include <QInputDialog>
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <libcockatrice/game/abstract_game.h>
|
||||
|
|
@ -20,6 +25,7 @@
|
|||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/game/player/player_manager.h>
|
||||
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
|
||||
class QWidget;
|
||||
|
|
@ -27,6 +33,7 @@ class QWidget;
|
|||
HandMenu::HandMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player)
|
||||
{
|
||||
auto *actions = player->getLogic()->getPlayerActions();
|
||||
connect(actions, &PlayerActions::randomHandRevealFailed, this, &HandMenu::onRandomHandRevealFailed);
|
||||
|
||||
if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) {
|
||||
aViewHand = new QAction(this);
|
||||
|
|
@ -67,6 +74,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 +162,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 +251,80 @@ 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<QAction *>(sender());
|
||||
const int handSize = player->getLogic()->getHandZone()->getCards().size();
|
||||
if (!action || handSize == 0) {
|
||||
return;
|
||||
}
|
||||
const int targetId = action->data().toInt();
|
||||
QPointer<PlayerActions> actions = player->getLogic()->getPlayerActions();
|
||||
QPointer<AbstractClient> client =
|
||||
player->getLogic()->getGame()->getClientForPlayer(player->getLogic()->getPlayerInfo()->getId());
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
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(this, &QObject::destroyed, dialog, &QDialog::reject);
|
||||
connect(client, &QObject::destroyed, dialog, &QDialog::reject);
|
||||
connect(actions, &QObject::destroyed, dialog, &QDialog::reject);
|
||||
connect(dialog, &QDialog::accepted, this, [dialog, actions, client, targetId] {
|
||||
if (!actions || !client) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
void HandMenu::onRandomHandRevealFailed(bool unsupported)
|
||||
{
|
||||
const QString text =
|
||||
unsupported ? tr("This server does not support revealing multiple random cards. No cards were revealed.")
|
||||
: tr("The random reveal could not be completed. The hand or recipient may have changed.");
|
||||
auto *message =
|
||||
new QMessageBox(QMessageBox::Warning, tr("Reveal Random Hand Cards"), text, QMessageBox::Ok, dialogParent());
|
||||
message->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(this, &QObject::destroyed, message, &QDialog::reject);
|
||||
connect(player->getLogic()->getPlayerActions(), &QObject::destroyed, message, &QDialog::reject);
|
||||
if (auto *client =
|
||||
player->getLogic()->getGame()->getClientForPlayer(player->getLogic()->getPlayerInfo()->getId())) {
|
||||
connect(client, &QObject::destroyed, message, &QDialog::reject);
|
||||
}
|
||||
message->open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,12 @@ private slots:
|
|||
void populateRevealRandomHandCardMenuWithActivePlayers();
|
||||
void onRevealHandTriggered();
|
||||
void onRevealRandomHandCardTriggered();
|
||||
void populateRevealRandomHandCardsMenuWithActivePlayers();
|
||||
void onRevealRandomHandCardsTriggered();
|
||||
void onRandomHandRevealFailed(bool unsupported);
|
||||
|
||||
private:
|
||||
QWidget *dialogParent() const;
|
||||
PlayerGraphicsItem *player;
|
||||
|
||||
QAction *aViewHand = nullptr;
|
||||
|
|
@ -62,6 +66,9 @@ private:
|
|||
QMenu *mRevealRandomHandCard = nullptr;
|
||||
QAction *aRevealRandomHandCardToAll = nullptr;
|
||||
|
||||
QMenu *mRevealRandomHandCards = nullptr;
|
||||
QAction *aRevealRandomHandCardsToAll = nullptr;
|
||||
|
||||
QMenu *mMoveHandMenu = nullptr;
|
||||
QAction *aMoveHandToTopLibrary = nullptr;
|
||||
QAction *aMoveHandToBottomLibrary = nullptr;
|
||||
|
|
|
|||
|
|
@ -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 request fails without a partial reveal. 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, and the client explains that it is unsupported. The existing **Reveal random card to...** action and its shortcut remain available for single-card reveals.
|
||||
|
|
@ -7,6 +7,10 @@
|
|||
- @subpage editing_decks
|
||||
- @subpage exporting_decks
|
||||
|
||||
## Gameplay
|
||||
|
||||
- @subpage revealing_cards
|
||||
|
||||
## Release Channels
|
||||
|
||||
- @subpage beta_release
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#include <QMetaType>
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
#include <QTimer>
|
||||
#include <QVariant>
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
#include <libcockatrice/deck_list/loaded_deck.h>
|
||||
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_change_zone_properties.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
|
||||
|
|
@ -66,6 +68,8 @@
|
|||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/clamped_arithmetic.h>
|
||||
#include <libcockatrice/utility/color.h>
|
||||
#include <libcockatrice/utility/counter_limits.h>
|
||||
|
|
@ -1756,6 +1760,27 @@ void PlayerActions::actRevealRandomHandCard(int revealToPlayerId)
|
|||
sendGameCommand(cmd);
|
||||
}
|
||||
|
||||
void PlayerActions::actRevealRandomHandCards(int revealToPlayerId, int amount)
|
||||
{
|
||||
QPointer<AbstractClient> client = player->getGame()->getClientForPlayer(player->getPlayerInfo()->getId());
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
Command_RevealRandomCards cmd;
|
||||
if (revealToPlayerId != -1) {
|
||||
cmd.set_player_id(revealToPlayerId);
|
||||
}
|
||||
cmd.set_zone_name(ZoneNames::HAND);
|
||||
cmd.set_number_of_cards(amount);
|
||||
auto *pending = prepareGameCommand(cmd);
|
||||
connect(pending, &PendingCommand::finished, this, [this, client](const Response &response) {
|
||||
if (client && response.response_code() != Response::RespOk) {
|
||||
emit randomHandRevealFailed(response.response_code() == Response::RespInvalidCommand);
|
||||
}
|
||||
});
|
||||
sendGameCommand(pending);
|
||||
}
|
||||
|
||||
void PlayerActions::actRevealLibrary(int revealToPlayerId)
|
||||
{
|
||||
Command_RevealCards cmd;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ public:
|
|||
}
|
||||
|
||||
signals:
|
||||
void randomHandRevealFailed(bool unsupported);
|
||||
void requestViewTopCardsDialog(int defaultNumberTopCards, int deckSize);
|
||||
void requestViewBottomCardsDialog(int defaultNumberBottomCards, int deckSize);
|
||||
void requestShuffleTopDialog(int defaultNumberTopCards, int maxCards);
|
||||
|
|
@ -279,6 +280,7 @@ public slots:
|
|||
void actReveal(QList<CardState *> selectedCards, QAction *action);
|
||||
void actRevealHand(int revealToPlayerId);
|
||||
void actRevealRandomHandCard(int revealToPlayerId);
|
||||
void actRevealRandomHandCards(int revealToPlayerId, int amount);
|
||||
void actRevealLibrary(int revealToPlayerId);
|
||||
|
||||
void actSortHand();
|
||||
|
|
|
|||
|
|
@ -400,7 +400,8 @@ Response::ResponseCode Server_AbstractParticipant::cmdDumpZone(const Command_Dum
|
|||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdRevealCards(const Command_RevealCards & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
GameEventStorage & /*ges*/,
|
||||
int /*randomCount*/)
|
||||
{
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
|
@ -544,6 +545,20 @@ 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: {
|
||||
const auto &random = command.GetExtension(Command_RevealRandomCards::ext);
|
||||
if (random.number_of_cards() <= 0) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
// Reuse the existing player/judge authorization and private reveal delivery.
|
||||
Command_RevealCards reveal;
|
||||
reveal.set_zone_name(random.zone_name());
|
||||
reveal.add_card_id(-2);
|
||||
if (random.has_player_id()) {
|
||||
reveal.set_player_id(random.player_id());
|
||||
}
|
||||
return cmdRevealCards(reveal, rc, ges, random.number_of_cards());
|
||||
}
|
||||
case GameCommand::MOVE_CARD:
|
||||
return cmdMoveCard(command.GetExtension(Command_MoveCard::ext), rc, ges);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ public:
|
|||
virtual Response::ResponseCode
|
||||
cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges, int randomCount = 1);
|
||||
virtual Response::ResponseCode
|
||||
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
|
|
|
|||
|
|
@ -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::cmdRevealCards(const Command_RevealCards &cmd,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage &ges,
|
||||
int randomCount)
|
||||
{
|
||||
if (!game->getGameStarted()) {
|
||||
return Response::RespGameNotStarted;
|
||||
|
|
@ -1532,11 +1534,18 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo
|
|||
//
|
||||
// This is to be compatible with clients supporting a single card_id
|
||||
// value, which send value -2 to request a random card.
|
||||
if (zone->getCards().isEmpty()) {
|
||||
if (randomCount <= 0 || randomCount > zone->getCards().size()) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
cardsToReveal.append(zone->getCards().at(rng->rand(0, zone->getCards().size() - 1)));
|
||||
// Partial Fisher-Yates on a copy: each physical card can appear only once,
|
||||
// and the hand's order and contents stay unchanged. Legacy requests use one.
|
||||
auto candidates = zone->getCards();
|
||||
for (int i = 0; i < randomCount; ++i) {
|
||||
const int selected = rng->rand(i, candidates.size() - 1);
|
||||
candidates.swapItemsAt(i, selected);
|
||||
cardsToReveal.append(candidates.at(i));
|
||||
}
|
||||
} else {
|
||||
for (auto cardId : cmd.card_id()) {
|
||||
Server_Card *card = zone->getCard(cardId);
|
||||
|
|
|
|||
|
|
@ -161,8 +161,10 @@ public:
|
|||
cmdIncCardCounter(const Command_IncCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
virtual Response::ResponseCode
|
||||
cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
virtual Response::ResponseCode
|
||||
cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
virtual Response::ResponseCode cmdRevealCards(const Command_RevealCards &cmd,
|
||||
ResponseContainer &rc,
|
||||
GameEventStorage &ges,
|
||||
int randomCount = 1) override;
|
||||
virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd,
|
||||
ResponseContainer &rc,
|
||||
GameEventStorage &ges) override;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -351,6 +351,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
|
||||
|
|
|
|||
246
tests/random_reveal_test.cpp
Normal file
246
tests/random_reveal_test.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#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 <QCoreApplication>
|
||||
#include <QSet>
|
||||
#include <google/protobuf/descriptor.pb.h>
|
||||
#include <google/protobuf/dynamic_message.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/get_pb_extension.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_reveal_cards.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_reveal_cards.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_zone.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <memory>
|
||||
|
||||
RNG_Abstract *rng = nullptr;
|
||||
|
||||
namespace
|
||||
{
|
||||
class AlternatingRng : public RNG_Abstract
|
||||
{
|
||||
int calls = 0;
|
||||
|
||||
public:
|
||||
unsigned int rand(int min, int max) override
|
||||
{
|
||||
EXPECT_LE(min, max);
|
||||
return calls++ % 2 == 0 && min < max ? min + 1 : min;
|
||||
}
|
||||
};
|
||||
|
||||
class Recipient : public Server_AbstractUserInterface
|
||||
{
|
||||
public:
|
||||
QList<Event_RevealCards> reveals;
|
||||
Recipient(Server *server, const char *name) : Server_AbstractUserInterface(server)
|
||||
{
|
||||
ServerInfo_User user;
|
||||
user.set_name(name);
|
||||
setUserInfo(user);
|
||||
}
|
||||
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 &container) override
|
||||
{
|
||||
for (const auto &event : container.event_list()) {
|
||||
if (event.HasExtension(Event_RevealCards::ext)) {
|
||||
reveals.append(event.GetExtension(Event_RevealCards::ext));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
GameCommand randomReveal(int count, int recipient = -1)
|
||||
{
|
||||
GameCommand command;
|
||||
auto *request = command.MutableExtension(Command_RevealRandomCards::ext);
|
||||
request->set_zone_name(ZoneNames::HAND);
|
||||
request->set_number_of_cards(count);
|
||||
if (recipient != -1) {
|
||||
request->set_player_id(recipient);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(RandomReveal, DistinctSamplesPrivacyAndCompatibility)
|
||||
{
|
||||
int argc = 1;
|
||||
char name[] = "random-reveal-test";
|
||||
char *argv[] = {name, nullptr};
|
||||
std::unique_ptr<QCoreApplication> application;
|
||||
if (!QCoreApplication::instance()) {
|
||||
application = std::make_unique<QCoreApplication>(argc, argv);
|
||||
}
|
||||
AlternatingRng random;
|
||||
rng = &random;
|
||||
FakeServer server;
|
||||
Recipient owner(&server, "Owner"), target(&server, "Target"), bystander(&server, "Bystander"),
|
||||
judge(&server, "Judge"), spectator(&server, "Spectator");
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
GameConfig config;
|
||||
config.creatorInfo = *owner.getUserInfo();
|
||||
config.gameId = 1;
|
||||
config.maxPlayers = 3;
|
||||
config.spectatorsAllowed = true;
|
||||
config.spectatorsSeeEverything = false;
|
||||
config.shareDecklistsOnLoad = false;
|
||||
Server_Game game(config, &room);
|
||||
for (auto *recipient : {&owner, &target, &bystander}) {
|
||||
ResponseContainer response(1);
|
||||
game.addPlayer(recipient, response, false, false, false);
|
||||
}
|
||||
ResponseContainer judgeResponse(1);
|
||||
game.addPlayer(&judge, judgeResponse, true, true, false);
|
||||
ResponseContainer spectatorResponse(1);
|
||||
game.addPlayer(&spectator, spectatorResponse, true, false, false);
|
||||
auto *player = game.getPlayer(0);
|
||||
ASSERT_NE(player, nullptr);
|
||||
ASSERT_NE(game.getPlayer(1), nullptr);
|
||||
ResponseContainer response(2);
|
||||
GameEventStorage notStarted;
|
||||
EXPECT_EQ(player->processGameCommand(randomReveal(2, 1), response, notStarted), Response::RespGameNotStarted);
|
||||
EXPECT_TRUE(notStarted.getGameEventList().isEmpty());
|
||||
for (auto *participant : game.getPlayers()) {
|
||||
Command_DeckSelect select;
|
||||
select.set_deck(DeckList().writeToString_Native().toStdString());
|
||||
GameCommand command;
|
||||
command.MutableExtension(Command_DeckSelect::ext)->CopyFrom(select);
|
||||
GameEventStorage events;
|
||||
EXPECT_EQ(participant->processGameCommand(command, response, events), Response::RespOk);
|
||||
participant->setReadyStart(true);
|
||||
}
|
||||
game.startGameIfReady(false);
|
||||
QCoreApplication::processEvents();
|
||||
ASSERT_TRUE(game.getGameStarted());
|
||||
auto *hand = player->getZones().value(ZoneNames::HAND);
|
||||
ASSERT_NE(hand, nullptr);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
auto *card = new Server_Card({i < 2 ? "Twin" : "Other", QString::number(i)}, player->newCardId(), i, 0);
|
||||
hand->insertCard(card, -1, 0);
|
||||
}
|
||||
const auto original = hand->getCards();
|
||||
GameEventStorage privateReveal;
|
||||
EXPECT_EQ(player->processGameCommand(randomReveal(2, 1), response, privateReveal), Response::RespOk);
|
||||
ASSERT_EQ(privateReveal.getGameEventList().size(), 2);
|
||||
EXPECT_EQ(privateReveal.getPrivatePlayerId(), 1);
|
||||
privateReveal.sendToGame(&game);
|
||||
ASSERT_EQ(owner.reveals.size(), 1);
|
||||
ASSERT_EQ(target.reveals.size(), 1);
|
||||
ASSERT_EQ(bystander.reveals.size(), 1);
|
||||
ASSERT_EQ(judge.reveals.size(), 1);
|
||||
ASSERT_EQ(spectator.reveals.size(), 1);
|
||||
EXPECT_EQ(target.reveals.last().cards_size(), 2);
|
||||
EXPECT_EQ(judge.reveals.last().cards_size(), 2); // existing judge visibility is retained
|
||||
EXPECT_NE(target.reveals.last().cards(0).id(), target.reveals.last().cards(1).id());
|
||||
EXPECT_EQ(target.reveals.last().cards(0).id(), original.at(1)->getId());
|
||||
EXPECT_EQ(target.reveals.last().cards(1).id(), original.at(0)->getId());
|
||||
EXPECT_EQ(target.reveals.last().cards(0).name(),
|
||||
target.reveals.last().cards(1).name()); // physical copies are distinct
|
||||
for (auto *recipient : {&owner, &bystander, &spectator}) {
|
||||
EXPECT_EQ(recipient->reveals.last().cards_size(), 0);
|
||||
EXPECT_EQ(recipient->reveals.last().number_of_cards(), 2);
|
||||
ASSERT_EQ(recipient->reveals.last().card_id_size(), 1);
|
||||
EXPECT_EQ(recipient->reveals.last().card_id(0), -2); // no actual card ids leak
|
||||
EXPECT_EQ(recipient->reveals.last().other_player_id(), 1);
|
||||
}
|
||||
EXPECT_EQ(hand->getCards(), original);
|
||||
GameEventStorage publicReveal;
|
||||
EXPECT_EQ(player->processGameCommand(randomReveal(4), response, publicReveal), Response::RespOk);
|
||||
publicReveal.sendToGame(&game);
|
||||
for (auto *recipient : {&owner, &target, &bystander, &judge, &spectator}) {
|
||||
ASSERT_EQ(recipient->reveals.last().cards_size(), 4);
|
||||
QSet<int> ids;
|
||||
for (const auto &card : recipient->reveals.last().cards()) {
|
||||
ids.insert(card.id());
|
||||
}
|
||||
EXPECT_EQ(ids.size(), 4);
|
||||
}
|
||||
EXPECT_EQ(hand->getCards(), original);
|
||||
for (int amount : {0, -1, 5, 2147483647}) {
|
||||
GameEventStorage invalid;
|
||||
EXPECT_NE(player->processGameCommand(randomReveal(amount, 1), response, invalid), Response::RespOk);
|
||||
EXPECT_TRUE(invalid.getGameEventList().isEmpty());
|
||||
}
|
||||
GameEventStorage missingRecipient;
|
||||
EXPECT_EQ(player->processGameCommand(randomReveal(2, 999), response, missingRecipient), Response::RespNameNotFound);
|
||||
EXPECT_TRUE(missingRecipient.getGameEventList().isEmpty());
|
||||
player->setConceded(true);
|
||||
GameEventStorage conceded;
|
||||
EXPECT_EQ(player->processGameCommand(randomReveal(2, 1), response, conceded), Response::RespContextError);
|
||||
EXPECT_TRUE(conceded.getGameEventList().isEmpty());
|
||||
player->setConceded(false);
|
||||
GameCommand legacy;
|
||||
auto *single = legacy.MutableExtension(Command_RevealCards::ext);
|
||||
single->set_zone_name(ZoneNames::HAND);
|
||||
single->add_card_id(-2);
|
||||
single->set_player_id(1);
|
||||
GameEventStorage legacyReveal;
|
||||
EXPECT_EQ(player->processGameCommand(legacy, response, legacyReveal), Response::RespOk);
|
||||
legacyReveal.sendToGame(&game);
|
||||
EXPECT_EQ(target.reveals.last().cards_size(), 1);
|
||||
EXPECT_EQ(owner.reveals.last().cards_size(), 0);
|
||||
|
||||
// A reader with only the old GameCommand schema keeps the new extension unknown.
|
||||
// Existing dispatch returns RespInvalidCommand; it cannot fall back to reveal-all.
|
||||
google::protobuf::FileDescriptorProto file;
|
||||
GameCommand::descriptor()->file()->CopyTo(&file);
|
||||
auto *values = file.mutable_message_type(0)->mutable_enum_type(0)->mutable_value();
|
||||
for (int i = values->size() - 1; i >= 0; --i) {
|
||||
if (values->Get(i).number() == 1036) {
|
||||
values->DeleteSubrange(i, 1);
|
||||
}
|
||||
}
|
||||
google::protobuf::DescriptorPool pool;
|
||||
ASSERT_NE(pool.BuildFile(file), nullptr);
|
||||
google::protobuf::FileDescriptorProto legacyRevealFile;
|
||||
Command_RevealCards::descriptor()->file()->CopyTo(&legacyRevealFile);
|
||||
auto *messages = legacyRevealFile.mutable_message_type();
|
||||
for (int i = messages->size() - 1; i >= 0; --i) {
|
||||
if (messages->Get(i).name() == "Command_RevealRandomCards") {
|
||||
messages->DeleteSubrange(i, 1);
|
||||
}
|
||||
}
|
||||
ASSERT_NE(pool.BuildFile(legacyRevealFile), nullptr);
|
||||
google::protobuf::DynamicMessageFactory factory(&pool);
|
||||
std::unique_ptr<google::protobuf::Message> old(
|
||||
factory.GetPrototype(pool.FindMessageTypeByName("GameCommand"))->New());
|
||||
ASSERT_TRUE(old->ParseFromString(legacy.SerializeAsString()));
|
||||
EXPECT_EQ(getPbExtension(*old), GameCommand::REVEAL_CARDS);
|
||||
old->Clear();
|
||||
ASSERT_TRUE(old->ParseFromString(randomReveal(2, 1).SerializeAsString()));
|
||||
EXPECT_EQ(getPbExtension(*old), -1);
|
||||
GameCommand unknown;
|
||||
unknown.GetReflection()->MutableUnknownFields(&unknown)->MergeFrom(old->GetReflection()->GetUnknownFields(*old));
|
||||
GameEventStorage unsupported;
|
||||
EXPECT_EQ(player->processGameCommand(unknown, response, unsupported), Response::RespInvalidCommand);
|
||||
EXPECT_TRUE(unsupported.getGameEventList().isEmpty());
|
||||
rng = nullptr;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue