mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Server/Client/Protocol] Shuffle or reorder player seats before a game starts (#7398)
* [Protocol] Add shuffle players option and player order command/event Adds a 'Shuffle players at start' option to game creation plus a local player id on the game state so clients can reconcile seat changes, and a SET_PLAYER_ORDER command (1206) with a PLAYER_ORDER_CHANGED event (2023) so the game host can set or randomize the seating order. Took 6 minutes * [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. * [Client] Add shuffle players option and turn order dialog Adds the 'Shuffle players at start' checkbox to game creation and a host-only 'Set turn order...' action that opens a dialog to reorder or randomize the seats before the game starts. Seating changes are shown in the game's message log. * [Client] Move the turn order dialog strings into retranslateUi Follows the DlgInviteToGame convention so the dialog can be retranslated instead of freezing its strings at construction time. * [Server] Keep player ids stable across seat shuffles * [Client] Lay out table and turn order dialog in seat order * [Server] Send game-joined player list in seat order * [Client] Reword the shuffle option to match its behaviour * [Client] Address the seat-order review comments - Give the game setup checkboxes a row of their own so they no longer collide in the create-game dialog - Rename DlgTurnOrder::randomize() to isRandomizeRequested() - Document what the ids in GameState::seatOrder mean - Keep "Set turn order..." in sync with the game: hidden for non-hosts and once the game has started, re-shown when the game ends or hosting moves to the local player (new GameState::hostChanged signal) * [Client] Do not preview a randomized turn order Randomizing is a request to the server, not a local shuffle. Shuffling the list locally meant the dialog showed an order the server then discarded, and let the host re-open it until a favourable order appeared. The dialog now switches to a mode that says the server will randomize and toggles back to the manual list on request. * [Client] Let moderators set the turn order The server accepts SET_PLAYER_ORDER from the host or a moderator, but the client hid the action from everyone except the host, making the moderator branch unreachable. Visibility and the handler now share canSetTurnOrder(). * [Server] Rely on the player order event instead of a full state resend cmdSetPlayerOrder re-sent the whole game state after reseating, so every client reloaded its deck view mid-lobby for no reason. The new seating reaches clients through the Event_PlayerOrderChanged that reseatPlayers broadcasts. * [Client] Show random turn order in the game selector The restrictions column listed "open decklists" but not the new seating option, so a joining player had no way to see that seats will be randomized each game. Game info already shows the disabled checkbox. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
ca2301bc96
commit
5ea16c805f
34 changed files with 1054 additions and 15 deletions
|
|
@ -56,6 +56,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||
src/interface/widgets/dialogs/dlg_turn_order.cpp
|
||||
src/interface/widgets/dialogs/dlg_update.cpp
|
||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
#include <functional>
|
||||
#include <libcockatrice/game/abstract_game.h>
|
||||
#include <libcockatrice/game/board/arrow_data.h>
|
||||
#include <libcockatrice/game/game_state.h>
|
||||
#include <libcockatrice/game/player/player_actions.h>
|
||||
#include <libcockatrice/game/player/player_event_handler.h>
|
||||
#include <libcockatrice/game/player/player_info.h>
|
||||
|
|
@ -441,6 +442,37 @@ QList<PlayerLogic *> GameScene::collectActivePlayers(int &firstPlayerIndex) cons
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lay out the table in seat order rather than id order. Players missing
|
||||
// from the tracked order keep id order at the end.
|
||||
QList<int> seatOrder;
|
||||
for (auto *pgItem : playerViews.values()) {
|
||||
if (PlayerLogic *p = pgItem->getLogic()) {
|
||||
seatOrder = p->getGame()->getGameState()->getSeatOrder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!seatOrder.isEmpty()) {
|
||||
std::sort(activePlayers.begin(), activePlayers.end(), [&](PlayerLogic *a, PlayerLogic *b) {
|
||||
int indexA = seatOrder.indexOf(a->getPlayerInfo()->getId());
|
||||
int indexB = seatOrder.indexOf(b->getPlayerInfo()->getId());
|
||||
if (indexA == -1) {
|
||||
indexA = seatOrder.size() + a->getPlayerInfo()->getId();
|
||||
}
|
||||
if (indexB == -1) {
|
||||
indexB = seatOrder.size() + b->getPlayerInfo()->getId();
|
||||
}
|
||||
return indexA < indexB;
|
||||
});
|
||||
// Recompute the local-player index after sorting.
|
||||
firstPlayerIndex = 0;
|
||||
for (int i = 0; i < activePlayers.size(); ++i) {
|
||||
if (activePlayers[i]->getPlayerInfo()->getLocal()) {
|
||||
firstPlayerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return activePlayers;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -605,6 +605,20 @@ void MessageLogWidget::logReverseTurn(PlayerLogic *player, bool reversed)
|
|||
.arg(reversed ? tr("reversed") : tr("normal")));
|
||||
}
|
||||
|
||||
void MessageLogWidget::logTurnOrderChanged(QStringList playerNames, bool randomized)
|
||||
{
|
||||
QStringList sanitizedNames;
|
||||
for (const QString &name : playerNames) {
|
||||
sanitizedNames.append(sanitizeHtml(name));
|
||||
}
|
||||
const QString order = sanitizedNames.join(" -> ");
|
||||
if (randomized) {
|
||||
appendHtmlServerMessage(tr("Randomized turn order: %1.").arg(order));
|
||||
} else {
|
||||
appendHtmlServerMessage(tr("New turn order set: %1.").arg(order));
|
||||
}
|
||||
}
|
||||
|
||||
void MessageLogWidget::logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls)
|
||||
{
|
||||
if (rolls.length() == 1) {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ public slots:
|
|||
int amount,
|
||||
bool isLentToAnotherPlayer);
|
||||
void logReverseTurn(PlayerLogic *player, bool reversed);
|
||||
void logTurnOrderChanged(QStringList playerNames, bool randomized);
|
||||
void logRollDie(PlayerLogic *player, int sides, const QList<uint> &rolls);
|
||||
void logSay(PlayerLogic *player, QString message);
|
||||
void logSetActivePhase(int phase);
|
||||
|
|
|
|||
|
|
@ -116,14 +116,17 @@ void DlgCreateGame::sharedCtor()
|
|||
|
||||
shareDecklistsOnLoadCheckBox = new QCheckBox(tr("Open decklists in lobby"));
|
||||
|
||||
shufflePlayersCheckBox = new QCheckBox(tr("Randomize turn order (seats) on each game start"));
|
||||
|
||||
createGameAsJudgeCheckBox = new QCheckBox(tr("Create game as judge"));
|
||||
|
||||
auto *gameSetupOptionsLayout = new QGridLayout;
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
|
||||
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
|
||||
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0, 1, 2);
|
||||
gameSetupOptionsLayout->addWidget(shufflePlayersCheckBox, 2, 0, 1, 2);
|
||||
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 3, 0, 1, 2);
|
||||
} else {
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
createGameAsJudgeCheckBox->setHidden(true);
|
||||
|
|
@ -184,6 +187,7 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap<int, QString> &_gameType
|
|||
createGameAsSpectatorCheckBox->setChecked(SettingsCache::instance().game().getCreateGameAsSpectator());
|
||||
startingLifeTotalEdit->setValue(SettingsCache::instance().game().getDefaultStartingLifeTotal());
|
||||
shareDecklistsOnLoadCheckBox->setChecked(SettingsCache::instance().game().getShareDecklistsOnLoad());
|
||||
shufflePlayersCheckBox->setChecked(SettingsCache::instance().game().getShufflePlayers());
|
||||
|
||||
if (!rememberGameSettings->isChecked()) {
|
||||
actReset();
|
||||
|
|
@ -217,6 +221,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
|
|||
createGameAsSpectatorCheckBox->setEnabled(false);
|
||||
startingLifeTotalEdit->setEnabled(false);
|
||||
shareDecklistsOnLoadCheckBox->setEnabled(false);
|
||||
shufflePlayersCheckBox->setEnabled(false);
|
||||
|
||||
descriptionEdit->setText(QString::fromStdString(gameInfo.description()));
|
||||
maxPlayersEdit->setValue(gameInfo.max_players());
|
||||
|
|
@ -227,6 +232,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
|
|||
spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat());
|
||||
spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient());
|
||||
shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load());
|
||||
shufflePlayersCheckBox->setChecked(gameInfo.shuffle_players());
|
||||
|
||||
QSet<int> types;
|
||||
for (int i = 0; i < gameInfo.game_types_size(); ++i) {
|
||||
|
|
@ -264,6 +270,7 @@ void DlgCreateGame::actReset()
|
|||
|
||||
startingLifeTotalEdit->setValue(20);
|
||||
shareDecklistsOnLoadCheckBox->setChecked(false);
|
||||
shufflePlayersCheckBox->setChecked(false);
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
|
||||
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
|
||||
|
|
@ -294,6 +301,7 @@ void DlgCreateGame::actOK()
|
|||
cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked());
|
||||
cmd.set_starting_life_total(startingLifeTotalEdit->value());
|
||||
cmd.set_share_decklists_on_load(shareDecklistsOnLoadCheckBox->isChecked());
|
||||
cmd.set_shuffle_players(shufflePlayersCheckBox->isChecked());
|
||||
|
||||
auto _gameTypes = QString();
|
||||
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
|
||||
|
|
@ -318,6 +326,7 @@ void DlgCreateGame::actOK()
|
|||
SettingsCache::instance().game().setCreateGameAsSpectator(createGameAsSpectatorCheckBox->isChecked());
|
||||
SettingsCache::instance().game().setDefaultStartingLifeTotal(startingLifeTotalEdit->value());
|
||||
SettingsCache::instance().game().setShareDecklistsOnLoad(shareDecklistsOnLoadCheckBox->isChecked());
|
||||
SettingsCache::instance().game().setShufflePlayers(shufflePlayersCheckBox->isChecked());
|
||||
SettingsCache::instance().game().setGameTypes(_gameTypes);
|
||||
}
|
||||
PendingCommand *pend = room->prepareRoomCommand(cmd);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ private:
|
|||
QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox,
|
||||
*spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox;
|
||||
QCheckBox *shareDecklistsOnLoadCheckBox;
|
||||
QCheckBox *shufflePlayersCheckBox;
|
||||
QDialogButtonBox *buttonBox;
|
||||
QPushButton *clearButton;
|
||||
QCheckBox *rememberGameSettings;
|
||||
|
|
|
|||
136
cockatrice/src/interface/widgets/dialogs/dlg_turn_order.cpp
Normal file
136
cockatrice/src/interface/widgets/dialogs/dlg_turn_order.cpp
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#include "dlg_turn_order.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
DlgTurnOrder::DlgTurnOrder(const QStringList &_playerNames, QWidget *parent)
|
||||
: QDialog(parent), randomizeRequested(false)
|
||||
{
|
||||
listWidget = new QListWidget(this);
|
||||
listWidget->addItems(_playerNames);
|
||||
listWidget->setCurrentRow(0);
|
||||
connect(listWidget, &QListWidget::currentRowChanged, this, &DlgTurnOrder::selectionChanged);
|
||||
|
||||
moveUpButton = new QPushButton(this);
|
||||
connect(moveUpButton, &QPushButton::clicked, this, &DlgTurnOrder::actMoveUp);
|
||||
moveDownButton = new QPushButton(this);
|
||||
connect(moveDownButton, &QPushButton::clicked, this, &DlgTurnOrder::actMoveDown);
|
||||
randomizeButton = new QPushButton(this);
|
||||
connect(randomizeButton, &QPushButton::clicked, this, &DlgTurnOrder::actRandomize);
|
||||
|
||||
// Shown in place of the list while the server is asked to randomize: the
|
||||
// order is chosen server side, so there is nothing to preview here.
|
||||
hintLabel = new QLabel(this);
|
||||
hintLabel->setWordWrap(true);
|
||||
hintLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto *buttonColumn = new QVBoxLayout;
|
||||
buttonColumn->addWidget(moveUpButton);
|
||||
buttonColumn->addWidget(moveDownButton);
|
||||
buttonColumn->addWidget(randomizeButton);
|
||||
buttonColumn->addStretch();
|
||||
|
||||
auto *listLayout = new QHBoxLayout;
|
||||
listLayout->addWidget(listWidget);
|
||||
listLayout->addWidget(hintLabel, 1);
|
||||
listLayout->addLayout(buttonColumn);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgTurnOrder::actOk);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgTurnOrder::reject);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(listLayout);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
setLayout(mainLayout);
|
||||
|
||||
updateButtons();
|
||||
|
||||
updateMode();
|
||||
setMinimumWidth(300);
|
||||
}
|
||||
|
||||
void DlgTurnOrder::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Set turn order"));
|
||||
moveUpButton->setText(tr("Move up"));
|
||||
moveDownButton->setText(tr("Move down"));
|
||||
randomizeButton->setText(randomizeRequested ? tr("Set order manually") : tr("Randomize"));
|
||||
hintLabel->setText(tr("The server will randomize the turn order."));
|
||||
}
|
||||
|
||||
QStringList DlgTurnOrder::order() const
|
||||
{
|
||||
QStringList playerNames;
|
||||
for (int i = 0; i < listWidget->count(); ++i) {
|
||||
playerNames.append(listWidget->item(i)->text());
|
||||
}
|
||||
return playerNames;
|
||||
}
|
||||
|
||||
void DlgTurnOrder::moveSelected(int delta)
|
||||
{
|
||||
const int row = listWidget->currentRow();
|
||||
const int newRow = row + delta;
|
||||
if (row < 0 || newRow < 0 || newRow >= listWidget->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QListWidgetItem *item = listWidget->takeItem(row);
|
||||
listWidget->insertItem(newRow, item);
|
||||
listWidget->setCurrentRow(newRow);
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void DlgTurnOrder::updateMode()
|
||||
{
|
||||
listWidget->setVisible(!randomizeRequested);
|
||||
hintLabel->setVisible(randomizeRequested);
|
||||
moveUpButton->setVisible(!randomizeRequested);
|
||||
moveDownButton->setVisible(!randomizeRequested);
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void DlgTurnOrder::updateButtons()
|
||||
{
|
||||
const int row = listWidget->currentRow();
|
||||
const int count = listWidget->count();
|
||||
const bool manualOrder = !randomizeRequested;
|
||||
moveUpButton->setEnabled(manualOrder && row > 0);
|
||||
moveDownButton->setEnabled(manualOrder && row >= 0 && row < count - 1);
|
||||
}
|
||||
|
||||
void DlgTurnOrder::actMoveUp()
|
||||
{
|
||||
moveSelected(-1);
|
||||
}
|
||||
|
||||
void DlgTurnOrder::actMoveDown()
|
||||
{
|
||||
moveSelected(1);
|
||||
}
|
||||
|
||||
void DlgTurnOrder::actRandomize()
|
||||
{
|
||||
// Randomizing is a request to the server, not a local shuffle: showing a
|
||||
// preview would let the host re-open the dialog until the order looks
|
||||
// favourable for them.
|
||||
randomizeRequested = !randomizeRequested;
|
||||
updateMode();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void DlgTurnOrder::actOk()
|
||||
{
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgTurnOrder::selectionChanged()
|
||||
{
|
||||
updateButtons();
|
||||
}
|
||||
51
cockatrice/src/interface/widgets/dialogs/dlg_turn_order.h
Normal file
51
cockatrice/src/interface/widgets/dialogs/dlg_turn_order.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @file dlg_turn_order.h
|
||||
* @ingroup GameDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_TURN_ORDER_H
|
||||
#define DLG_TURN_ORDER_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QStringList>
|
||||
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class DlgTurnOrder : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QListWidget *listWidget;
|
||||
QLabel *hintLabel;
|
||||
QPushButton *moveUpButton;
|
||||
QPushButton *moveDownButton;
|
||||
QPushButton *randomizeButton;
|
||||
bool randomizeRequested;
|
||||
|
||||
void moveSelected(int delta);
|
||||
void updateMode();
|
||||
void updateButtons();
|
||||
|
||||
private slots:
|
||||
void actMoveUp();
|
||||
void actMoveDown();
|
||||
void actRandomize();
|
||||
void actOk();
|
||||
void selectionChanged();
|
||||
|
||||
public:
|
||||
explicit DlgTurnOrder(const QStringList &_playerNames, QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] bool isRandomizeRequested() const
|
||||
{
|
||||
return randomizeRequested;
|
||||
}
|
||||
[[nodiscard]] QStringList order() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -185,6 +185,9 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const
|
|||
if (gameentry.share_decklists_on_load()) {
|
||||
result.append(tr("open decklists"));
|
||||
}
|
||||
if (gameentry.shuffle_players()) {
|
||||
result.append(tr("random turn order"));
|
||||
}
|
||||
return result.join(", ");
|
||||
}
|
||||
case Qt::DecorationRole: {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||
#include "../interface/widgets/dialogs/dlg_invite_to_game.h"
|
||||
#include "../interface/widgets/dialogs/dlg_turn_order.h"
|
||||
#include "../interface/widgets/server/game_link.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/utility/completer_utils.h"
|
||||
|
|
@ -195,6 +196,7 @@ void TabGame::connectToGameState()
|
|||
{
|
||||
connect(game->getGameState(), &GameState::gameStarted, this, &TabGame::startGame);
|
||||
connect(game->getGameState(), &GameState::gameStopped, this, &TabGame::stopGame);
|
||||
connect(game->getGameState(), &GameState::hostChanged, this, &TabGame::updateTurnOrderAction);
|
||||
connect(game->getGameState(), &GameState::activePhaseChanged, this, &TabGame::setActivePhase);
|
||||
connect(game->getGameState(), &GameState::activePlayerChanged, this, &TabGame::setActivePlayer);
|
||||
}
|
||||
|
|
@ -263,6 +265,8 @@ void TabGame::connectMessageLogToGameEventHandler()
|
|||
|
||||
connect(game->getGameEventHandler(), &GameEventHandler::logTurnReversed, messageLog,
|
||||
&MessageLogWidget::logReverseTurn);
|
||||
connect(game->getGameEventHandler(), &GameEventHandler::logTurnOrderChanged, messageLog,
|
||||
&MessageLogWidget::logTurnOrderChanged);
|
||||
|
||||
connect(game->getGameEventHandler(), &GameEventHandler::logConcede, messageLog, &MessageLogWidget::logConcede);
|
||||
connect(game->getGameEventHandler(), &GameEventHandler::logUnconcede, messageLog, &MessageLogWidget::logUnconcede);
|
||||
|
|
@ -373,6 +377,9 @@ void TabGame::retranslateUi()
|
|||
if (aReverseTurn) {
|
||||
aReverseTurn->setText(tr("Reverse turn order"));
|
||||
}
|
||||
if (aTurnOrder) {
|
||||
aTurnOrder->setText(tr("&Set turn order..."));
|
||||
}
|
||||
if (aRemoveLocalArrows) {
|
||||
aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
|
||||
}
|
||||
|
|
@ -629,6 +636,73 @@ void TabGame::actConcede()
|
|||
}
|
||||
}
|
||||
|
||||
void TabGame::actTurnOrder()
|
||||
{
|
||||
if (!canSetTurnOrder()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList playerNames;
|
||||
const QList<int> seatOrder = game->getGameState()->getSeatOrder();
|
||||
const auto &players = game->getPlayerManager()->getPlayers();
|
||||
if (!seatOrder.isEmpty()) {
|
||||
for (int playerId : seatOrder) {
|
||||
if (PlayerLogic *player = players.value(playerId, nullptr)) {
|
||||
playerNames.append(player->getPlayerInfo()->getName());
|
||||
}
|
||||
}
|
||||
// Include any players missing from the tracked order rather than dropping them.
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
const QString name = it.value()->getPlayerInfo()->getName();
|
||||
if (!playerNames.contains(name)) {
|
||||
playerNames.append(name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (PlayerLogic *player : players) {
|
||||
playerNames.append(player->getPlayerInfo()->getName());
|
||||
}
|
||||
}
|
||||
|
||||
DlgTurnOrder dlg(playerNames, this);
|
||||
if (dlg.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
game->getGameEventHandler()->handleSetPlayerOrder(dlg.order(), dlg.isRandomizeRequested());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the local user may change the seating order: only while the game has
|
||||
* not started, and only as the host or a moderator - the server allows both
|
||||
* (Server_AbstractParticipant::cmdSetPlayerOrder).
|
||||
*/
|
||||
bool TabGame::canSetTurnOrder() const
|
||||
{
|
||||
if (game->getGameMetaInfo()->started()) {
|
||||
return false;
|
||||
}
|
||||
if (game->isHost()) {
|
||||
return true;
|
||||
}
|
||||
const ServerInfo_User *userInfo = tabSupervisor->getUserInfo();
|
||||
return userInfo != nullptr && (userInfo->user_level() & ServerInfo_User::IsModerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the turn order action only when it can actually change anything: the
|
||||
* player must be allowed to set the order and the game must not have started.
|
||||
* Called whenever either condition may have changed (game start/stop, host
|
||||
* change).
|
||||
*/
|
||||
void TabGame::updateTurnOrderAction()
|
||||
{
|
||||
if (aTurnOrder == nullptr) {
|
||||
return;
|
||||
}
|
||||
aTurnOrder->setVisible(canSetTurnOrder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the leave game and sends the leave game command, if applicable.
|
||||
*
|
||||
|
|
@ -840,6 +914,15 @@ void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer)
|
|||
}
|
||||
}
|
||||
|
||||
// A shuffled local player has their seat changed, so the deck view container
|
||||
// keyed by their previous id must be cleaned up alongside the leaving player.
|
||||
int leavingPlayerId = leavingPlayer->getPlayerInfo()->getId();
|
||||
if (deckViewContainers.contains(leavingPlayerId)) {
|
||||
TabbedDeckViewContainer *deckView = deckViewContainers.take(leavingPlayerId);
|
||||
deckViewContainerLayout->removeWidget(deckView);
|
||||
deckView->deleteLater();
|
||||
}
|
||||
|
||||
scene->removePlayer(leavingPlayer);
|
||||
}
|
||||
|
||||
|
|
@ -948,6 +1031,7 @@ void TabGame::startGame(bool _resuming)
|
|||
aConcede->setText(tr("&Concede"));
|
||||
aConcede->setEnabled(true);
|
||||
}
|
||||
updateTurnOrderAction();
|
||||
}
|
||||
|
||||
void TabGame::stopGame()
|
||||
|
|
@ -970,6 +1054,7 @@ void TabGame::stopGame()
|
|||
aConcede->setText(tr("&Concede"));
|
||||
aConcede->setEnabled(false);
|
||||
}
|
||||
updateTurnOrderAction();
|
||||
}
|
||||
|
||||
void TabGame::closeGame()
|
||||
|
|
@ -1094,6 +1179,9 @@ void TabGame::createMenuItems()
|
|||
connect(aNextTurn, &QAction::triggered, game->getGameEventHandler(), &GameEventHandler::handleNextTurn);
|
||||
aReverseTurn = new QAction(this);
|
||||
connect(aReverseTurn, &QAction::triggered, game->getGameEventHandler(), &GameEventHandler::handleReverseTurn);
|
||||
aTurnOrder = new QAction(this);
|
||||
connect(aTurnOrder, &QAction::triggered, this, &TabGame::actTurnOrder);
|
||||
updateTurnOrderAction();
|
||||
aRemoveLocalArrows = new QAction(this);
|
||||
connect(aRemoveLocalArrows, &QAction::triggered, this, &TabGame::actRemoveLocalArrows);
|
||||
aRotateViewCW = new QAction(this);
|
||||
|
|
@ -1144,6 +1232,7 @@ void TabGame::createMenuItems()
|
|||
gameMenu->addAction(aNextTurn);
|
||||
aNextTurn->setObjectName("aNextTurn");
|
||||
gameMenu->addAction(aReverseTurn);
|
||||
gameMenu->addAction(aTurnOrder);
|
||||
gameMenu->addSeparator();
|
||||
gameMenu->addAction(aRemoveLocalArrows);
|
||||
gameMenu->addAction(aRotateViewCW);
|
||||
|
|
@ -1172,6 +1261,7 @@ void TabGame::createReplayMenuItems()
|
|||
aNextPhaseAction = nullptr;
|
||||
aNextTurn = nullptr;
|
||||
aReverseTurn = nullptr;
|
||||
aTurnOrder = nullptr;
|
||||
aRemoveLocalArrows = nullptr;
|
||||
aRotateViewCW = nullptr;
|
||||
aRotateViewCCW = nullptr;
|
||||
|
|
@ -1302,6 +1392,7 @@ void TabGame::createPlayAreaWidget(bool bReplay)
|
|||
scene = new GameScene(phasesToolbar, this);
|
||||
connect(game->getPlayerManager(), &PlayerManager::playerConceded, scene, &GameScene::rearrange);
|
||||
connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, scene, &GameScene::rearrange);
|
||||
connect(game->getGameState(), &GameState::seatOrderChanged, scene, &GameScene::rearrange);
|
||||
connect(scene, &GameScene::arrowDeletionRequested, game->getGameEventHandler(),
|
||||
&GameEventHandler::handleArrowDeletion);
|
||||
connect(game->getGameEventHandler(), &GameEventHandler::arrowDeleted, scene, &GameScene::deleteArrow);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ private:
|
|||
QMenu *gameMenu, *viewMenu;
|
||||
TearOffMenu *phasesMenu;
|
||||
QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn,
|
||||
*aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout;
|
||||
*aReverseTurn, *aTurnOrder, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout,
|
||||
*aResetReplayLayout;
|
||||
QAction *aFocusChat;
|
||||
QAction *aInviteToGame = nullptr;
|
||||
QList<QAction *> phaseActions;
|
||||
|
|
@ -129,6 +130,7 @@ private:
|
|||
|
||||
PlayerLogic *setActivePlayer(int id);
|
||||
void setActivePhase(int phase);
|
||||
bool canSetTurnOrder() const;
|
||||
void createMenuItems();
|
||||
void createReplayMenuItems();
|
||||
void createViewMenuItems();
|
||||
|
|
@ -162,6 +164,8 @@ private slots:
|
|||
void actInviteToGame();
|
||||
void actConcede();
|
||||
void actCopyGameLink();
|
||||
void actTurnOrder();
|
||||
void updateTurnOrderAction();
|
||||
void actRemoveLocalArrows();
|
||||
void actRotateViewCW();
|
||||
void actRotateViewCCW();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <QMap>
|
||||
#include <QMapIterator>
|
||||
#include <QMessageLogger>
|
||||
#include <QSet>
|
||||
#include <QSharedPointer>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <google/protobuf/message.h>
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_reverse_turn.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_player_order.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
|
|
@ -50,6 +52,7 @@
|
|||
#include <libcockatrice/protocol/pb/event_join.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_kicked.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_order_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_properties_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_reverse_turn.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_phase.pb.h>
|
||||
|
|
@ -189,6 +192,9 @@ void GameEventHandler::processGameEventContainer(const GameEventContainer &cont,
|
|||
case GameEvent::REVERSE_TURN:
|
||||
eventReverseTurn(event.GetExtension(Event_ReverseTurn::ext), playerId, context);
|
||||
break;
|
||||
case GameEvent::PLAYER_ORDER_CHANGED:
|
||||
eventPlayerOrderChanged(event.GetExtension(Event_PlayerOrderChanged::ext), playerId, context);
|
||||
break;
|
||||
|
||||
default: {
|
||||
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0);
|
||||
|
|
@ -215,6 +221,16 @@ void GameEventHandler::handleReverseTurn()
|
|||
sendGameCommand(Command_ReverseTurn());
|
||||
}
|
||||
|
||||
void GameEventHandler::handleSetPlayerOrder(const QList<QString> &playerNames, bool randomize)
|
||||
{
|
||||
Command_SetPlayerOrder command;
|
||||
command.set_randomize(randomize);
|
||||
for (const QString &name : playerNames) {
|
||||
command.add_player_names(name.toStdString());
|
||||
}
|
||||
sendGameCommand(command);
|
||||
}
|
||||
|
||||
void GameEventHandler::handleActiveLocalPlayerConceded()
|
||||
{
|
||||
sendGameCommand(Command_Concede());
|
||||
|
|
@ -377,6 +393,27 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
{
|
||||
const int playerListSize = event.player_list_size();
|
||||
|
||||
if (event.has_local_player_id()) {
|
||||
game->getPlayerManager()->setLocalPlayerId(event.local_player_id());
|
||||
}
|
||||
|
||||
QSet<int> authoritativePlayerIds;
|
||||
for (int i = 0; i < playerListSize; ++i) {
|
||||
const ServerInfo_PlayerProperties &prop = event.player_list(i).properties();
|
||||
if (!prop.spectator()) {
|
||||
authoritativePlayerIds.insert(prop.player_id());
|
||||
}
|
||||
}
|
||||
|
||||
// Players whose seats were shuffled away from the previous game are no longer
|
||||
// part of the authoritative list and must be removed.
|
||||
for (int stalePlayerId : game->getPlayerManager()->getPlayers().keys()) {
|
||||
if (!authoritativePlayerIds.contains(stalePlayerId)) {
|
||||
game->getPlayerManager()->removePlayer(stalePlayerId);
|
||||
emit playerLeft(stalePlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
QVector<QPair<int, QPair<QString, QString>>> opponentDecksToDisplay;
|
||||
|
||||
for (int i = 0; i < playerListSize; ++i) {
|
||||
|
|
@ -421,6 +458,19 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
|
||||
emit remotePlayersDecksSelected(opponentDecksToDisplay);
|
||||
|
||||
// The server emits player_list in seat order, so record it to lay out the
|
||||
// table by seating rather than by player id.
|
||||
QList<int> seatOrder;
|
||||
for (int i = 0; i < playerListSize; ++i) {
|
||||
const ServerInfo_PlayerProperties &prop = event.player_list(i).properties();
|
||||
if (!prop.spectator()) {
|
||||
seatOrder.append(prop.player_id());
|
||||
}
|
||||
}
|
||||
if (!seatOrder.isEmpty()) {
|
||||
game->getGameState()->setSeatOrder(seatOrder);
|
||||
}
|
||||
|
||||
game->getGameState()->setGameTime(event.seconds_elapsed());
|
||||
|
||||
if (event.game_started() && !game->getGameMetaInfo()->started()) {
|
||||
|
|
@ -625,6 +675,33 @@ void GameEventHandler::eventReverseTurn(const Event_ReverseTurn &event,
|
|||
emit logTurnReversed(player, event.reversed());
|
||||
}
|
||||
|
||||
void GameEventHandler::eventPlayerOrderChanged(const Event_PlayerOrderChanged &event,
|
||||
int /*eventPlayerId*/,
|
||||
const GameEventContext & /*context*/)
|
||||
{
|
||||
QStringList playerNames;
|
||||
for (const auto &name : event.player_names()) {
|
||||
playerNames.append(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
// Track the seating by stable player id so the table layout and the turn
|
||||
// order dialog follow the shuffled order instead of id order.
|
||||
QList<int> seatOrder;
|
||||
const auto &players = game->getPlayerManager()->getPlayers();
|
||||
for (const QString &name : playerNames) {
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
if (it.value() && it.value()->getPlayerInfo()->getName() == name) {
|
||||
seatOrder.append(it.key());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!seatOrder.isEmpty()) {
|
||||
game->getGameState()->setSeatOrder(seatOrder);
|
||||
}
|
||||
emit logTurnOrderChanged(playerNames, event.randomized());
|
||||
}
|
||||
|
||||
void GameEventHandler::eventGameHostChanged(const Event_GameHostChanged & /*event*/,
|
||||
int eventPlayerId,
|
||||
const GameEventContext & /*context*/)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Event_GameSay;
|
|||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class Event_Ping;
|
||||
class Event_PlayerOrderChanged;
|
||||
class ServerInfo_PlayerProperties;
|
||||
namespace google
|
||||
{
|
||||
|
|
@ -113,6 +114,16 @@ public:
|
|||
/** @brief Request reversing the current turn order. */
|
||||
void handleReverseTurn();
|
||||
|
||||
/**
|
||||
* @brief Request a new seating / turn order.
|
||||
*
|
||||
* Only the game host (or a moderator) may do this before the game starts.
|
||||
*
|
||||
* @param playerNames Ordered list of player names. Ignored when randomize is true.
|
||||
* @param randomize Ask the server to shuffle the seats instead.
|
||||
*/
|
||||
void handleSetPlayerOrder(const QList<QString> &playerNames, bool randomize);
|
||||
|
||||
/** @brief Concede the game for the currently active local player. */
|
||||
void handleActiveLocalPlayerConceded();
|
||||
|
||||
|
|
@ -316,6 +327,10 @@ public:
|
|||
/** @brief Handle a turn reversal event. */
|
||||
void eventReverseTurn(const Event_ReverseTurn &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle a change of the seating / turn order. */
|
||||
void
|
||||
eventPlayerOrderChanged(const Event_PlayerOrderChanged &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
/** @brief Handle ping / latency updates. */
|
||||
void eventPing(const Event_Ping &event, int eventPlayerId, const GameEventContext &context);
|
||||
|
||||
|
|
@ -443,6 +458,7 @@ signals:
|
|||
void logLeave(PlayerLogic *player, QString reason);
|
||||
void logKicked();
|
||||
void logTurnReversed(PlayerLogic *player, bool reversed);
|
||||
void logTurnOrderChanged(QStringList playerNames, bool randomized);
|
||||
void logGameClosed();
|
||||
void logActivePlayer(PlayerLogic *activePlayer);
|
||||
void logActivePhaseChanged(int activePhase);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ public:
|
|||
|
||||
void setHostId(int _hostId)
|
||||
{
|
||||
if (hostId == _hostId) {
|
||||
return;
|
||||
}
|
||||
hostId = _hostId;
|
||||
emit hostChanged(hostId);
|
||||
}
|
||||
|
||||
QList<AbstractClient *> getClients() const
|
||||
|
|
@ -112,6 +116,19 @@ public:
|
|||
return hostId;
|
||||
}
|
||||
|
||||
void setSeatOrder(const QList<int> &order)
|
||||
{
|
||||
if (seatOrder != order) {
|
||||
seatOrder = order;
|
||||
emit seatOrderChanged(seatOrder);
|
||||
}
|
||||
}
|
||||
|
||||
QList<int> getSeatOrder() const
|
||||
{
|
||||
return seatOrder;
|
||||
}
|
||||
|
||||
int getSecondsElapsed() const
|
||||
{
|
||||
return secondsElapsed;
|
||||
|
|
@ -123,6 +140,8 @@ signals:
|
|||
void gameStopped();
|
||||
void activePhaseChanged(int activePhase);
|
||||
void activePlayerChanged(int playerId);
|
||||
void hostChanged(int hostId);
|
||||
void seatOrderChanged(const QList<int> &seatOrder);
|
||||
|
||||
public slots:
|
||||
void incrementGameTime();
|
||||
|
|
@ -142,6 +161,9 @@ private:
|
|||
int currentPhase;
|
||||
int activePlayer;
|
||||
bool gameClosed;
|
||||
//! Player ids in seating (turn) order, first to play first. Empty until the
|
||||
//! server sends an order, in which case players are laid out in id order.
|
||||
QList<int> seatOrder;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_GAME_STATE_H
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ public:
|
|||
return localPlayerId;
|
||||
}
|
||||
|
||||
void setLocalPlayerId(int _localPlayerId)
|
||||
{
|
||||
localPlayerId = _localPlayerId;
|
||||
}
|
||||
|
||||
[[nodiscard]] const QMap<int, PlayerLogic *> &getPlayers() const
|
||||
{
|
||||
return players;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public:
|
|||
[[nodiscard]] virtual bool getCreateGameAsSpectator() const = 0;
|
||||
[[nodiscard]] virtual int getDefaultStartingLifeTotal() const = 0;
|
||||
[[nodiscard]] virtual bool getShareDecklistsOnLoad() const = 0;
|
||||
[[nodiscard]] virtual bool getShufflePlayers() const = 0;
|
||||
[[nodiscard]] virtual bool getRememberGameSettings() const = 0;
|
||||
[[nodiscard]] virtual bool getLocalGameRememberSettings() const = 0;
|
||||
[[nodiscard]] virtual int getLocalGameMaxPlayers() const = 0;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ struct GameConfig
|
|||
bool spectatorsSeeEverything = true;
|
||||
int startingLifeTotal = 20;
|
||||
bool shareDecklistsOnLoad = true;
|
||||
bool shufflePlayers = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_player_order.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
|
|
@ -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<QString> playerNames;
|
||||
for (int i = 0; i < cmd.player_names_size(); ++i) {
|
||||
playerNames.append(QString::fromStdString(cmd.player_names(i)));
|
||||
}
|
||||
game->reorderPlayerSeats(playerNames);
|
||||
}
|
||||
|
||||
// Clients learn the new seating from the Event_PlayerOrderChanged that
|
||||
// reseatPlayers broadcasts; no full game state resend is needed for it.
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -73,11 +73,13 @@
|
|||
#include <libcockatrice/protocol/pb/event_join.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_kicked.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_order_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_properties_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_replay_added.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.h>
|
||||
|
|
@ -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()
|
||||
|
|
@ -292,6 +294,32 @@ int Server_Game::getSpectatorCount() const
|
|||
return result;
|
||||
}
|
||||
|
||||
QList<Server_AbstractParticipant *> Server_Game::getParticipantsInSeatOrder() const
|
||||
{
|
||||
// Players in seat order first (fall back to id order for seats predating
|
||||
// seatOrder), then spectators. Player ids are stable; only the ordering
|
||||
// changes on shuffle/reorder.
|
||||
QList<Server_AbstractParticipant *> ordered;
|
||||
ordered.reserve(participants.size());
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator() && !ordered.contains(participant)) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !ordered.contains(participant)) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
ordered.append(participant);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
||||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
|
|
@ -306,16 +334,24 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
|||
event->set_game_started(false);
|
||||
}
|
||||
|
||||
for (Server_AbstractParticipant *participant : participants.values()) {
|
||||
// Emit the player list in seat order so every client (including old ones
|
||||
// that ignore Event_PlayerOrderChanged) sees the current seating. Player
|
||||
// ids are stable; only this ordering changes on shuffle/reorder.
|
||||
QList<Server_AbstractParticipant *> ordered = getParticipantsInSeatOrder();
|
||||
for (Server_AbstractParticipant *participant : ordered) {
|
||||
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 +363,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 +376,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 +416,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 +474,116 @@ void Server_Game::startGameIfReady(bool forceStartGame)
|
|||
emit sigStartGameIfReady(forceStartGame);
|
||||
}
|
||||
|
||||
void Server_Game::shufflePlayerSeats()
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Collect players in current seat order (fall back to id order for seats
|
||||
// predating seatOrder). Spectators keep their seats.
|
||||
QList<Server_AbstractParticipant *> players;
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator()) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !players.contains(participant)) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
QList<Server_AbstractParticipant *> spectators;
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
spectators.append(participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Fisher-Yates shuffle of the players.
|
||||
QList<Server_AbstractParticipant *> 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<QString> &orderedNames)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Collect players in current seat order (fall back to id order for seats
|
||||
// predating seatOrder). Spectators keep their seats.
|
||||
QList<Server_AbstractParticipant *> players;
|
||||
for (int id : seatOrder) {
|
||||
auto *participant = participants.value(id, nullptr);
|
||||
if (participant && !participant->isSpectator()) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
for (auto *participant : participants.values()) {
|
||||
if (!participant->isSpectator() && !players.contains(participant)) {
|
||||
players.append(participant);
|
||||
}
|
||||
}
|
||||
QList<Server_AbstractParticipant *> spectators;
|
||||
for (auto *participant : participants.values()) {
|
||||
if (participant->isSpectator()) {
|
||||
spectators.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<Server_AbstractParticipant *> orderedPlayers;
|
||||
QSet<Server_AbstractParticipant *> 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<Server_AbstractParticipant *> &orderedParticipants, bool randomized)
|
||||
{
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
// Keep player ids stable and only reorder the seating/turn order. Changing
|
||||
// ids would break old clients (stale localPlayerId from Event_GameJoined
|
||||
// duplicates their own seat as an opponent), invalidate the home-server
|
||||
// per-user game/seat mapping used for ISL command routing, and orphan the
|
||||
// host id. All of those maps stay valid because ids never change here.
|
||||
// The current host therefore keeps its id and needs no re-key tracking.
|
||||
seatOrder.clear();
|
||||
for (auto *participant : orderedParticipants) {
|
||||
if (!participant->isSpectator()) {
|
||||
seatOrder.append(participant->getPlayerId());
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -563,6 +714,7 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface,
|
|||
allSpectatorsEver.insert(playerName);
|
||||
} else {
|
||||
allPlayersEver.insert(playerName);
|
||||
seatOrder.append(newParticipant->getPlayerId());
|
||||
|
||||
// if the original creator of the game joins, give them host status back
|
||||
//! \todo Transferring host to spectators has side effects.
|
||||
|
|
@ -592,6 +744,7 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve
|
|||
room->getServer()->removePersistentPlayer(QString::fromStdString(participant->getUserInfo()->name()), room->getId(),
|
||||
gameId, participant->getPlayerId());
|
||||
participants.remove(participant->getPlayerId());
|
||||
seatOrder.removeAll(participant->getPlayerId());
|
||||
|
||||
bool spectator = participant->isSpectator();
|
||||
GameEventStorage ges;
|
||||
|
|
@ -611,10 +764,20 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve
|
|||
participant->prepareDestroy();
|
||||
|
||||
if (playerHost) {
|
||||
// Pass host to the next player in seat order so the game always keeps
|
||||
// a host while players remain.
|
||||
int newHostId = -1;
|
||||
for (auto *otherPlayer : getPlayers().values()) {
|
||||
newHostId = otherPlayer->getPlayerId();
|
||||
break;
|
||||
for (int id : seatOrder) {
|
||||
if (participants.contains(id)) {
|
||||
newHostId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newHostId == -1) {
|
||||
for (auto *otherPlayer : getPlayers().values()) {
|
||||
newHostId = otherPlayer->getPlayerId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newHostId != -1) {
|
||||
hostId = newHostId;
|
||||
|
|
@ -771,7 +934,20 @@ void Server_Game::nextTurn()
|
|||
}
|
||||
|
||||
auto players = getPlayers();
|
||||
const QList<int> keys = players.keys();
|
||||
// Advance in seat order, not id order, so shuffles/reorders change the
|
||||
// turn sequence while ids stay stable.
|
||||
QList<int> keys;
|
||||
keys.reserve(players.size());
|
||||
for (int id : seatOrder) {
|
||||
if (players.contains(id)) {
|
||||
keys.append(id);
|
||||
}
|
||||
}
|
||||
for (int id : players.keys()) {
|
||||
if (!keys.contains(id)) {
|
||||
keys.append(id);
|
||||
}
|
||||
}
|
||||
int listPos = -1;
|
||||
if (activePlayer != -1) {
|
||||
listPos = keys.indexOf(activePlayer);
|
||||
|
|
@ -821,7 +997,7 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
event2.set_active_phase(activePhase);
|
||||
|
||||
bool omniscient = (joiningParticipant->isSpectator() && spectatorsSeeEverything) || joiningParticipant->isJudge();
|
||||
for (auto *participant : participants.values()) {
|
||||
for (auto *participant : getParticipantsInSeatOrder()) {
|
||||
participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true);
|
||||
}
|
||||
|
||||
|
|
@ -950,6 +1126,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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ private:
|
|||
int hostId;
|
||||
ServerInfo_User *creatorInfo;
|
||||
QMap<int, Server_AbstractParticipant *> participants;
|
||||
// Seating/turn order as an ordered list of player ids. Player ids themselves
|
||||
// are stable for the lifetime of the participant; shuffling or reordering
|
||||
// only permutes this list so old clients, ISL routes and persistent
|
||||
// references (all keyed by player id) keep working.
|
||||
QList<int> seatOrder;
|
||||
QSet<QString> allPlayersEver, allSpectatorsEver;
|
||||
bool gameStarted;
|
||||
bool gameClosed;
|
||||
|
|
@ -90,6 +95,7 @@ private:
|
|||
bool spectatorsSeeEverything;
|
||||
int startingLifeTotal;
|
||||
bool shareDecklistsOnLoad;
|
||||
bool shufflePlayers;
|
||||
int inactivityCounter;
|
||||
int startTimeOfThisGame, secondsElapsed;
|
||||
bool firstGameStarted;
|
||||
|
|
@ -109,6 +115,8 @@ private:
|
|||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
bool withUserInfo);
|
||||
QList<Server_AbstractParticipant *> getParticipantsInSeatOrder() const;
|
||||
void reseatPlayers(const QList<Server_AbstractParticipant *> &orderedParticipants, bool randomized);
|
||||
void storeGameInformation();
|
||||
bool canSeePrivateEventsFor(const Server_AbstractParticipant *participant, int privatePlayerId) const;
|
||||
signals:
|
||||
|
|
@ -149,6 +157,10 @@ public:
|
|||
{
|
||||
return participants;
|
||||
}
|
||||
QList<int> getSeatOrder() const
|
||||
{
|
||||
return seatOrder;
|
||||
}
|
||||
int getGameId() const
|
||||
{
|
||||
return gameId;
|
||||
|
|
@ -189,6 +201,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 +218,8 @@ public:
|
|||
bool kickParticipant(int playerId);
|
||||
void startGameIfReady(bool forceStartGame);
|
||||
void stopGameIfFinished();
|
||||
void shufflePlayerSeats();
|
||||
void reorderPlayerSeats(const QList<QString> &orderedNames);
|
||||
int getActivePlayer() const
|
||||
{
|
||||
return activePlayer;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ set(PROTO_FILES
|
|||
command_set_card_attr.proto
|
||||
command_set_card_counter.proto
|
||||
command_set_counter.proto
|
||||
command_set_player_order.proto
|
||||
command_set_playmat.proto
|
||||
command_set_sideboard_lock.proto
|
||||
command_set_sideboard_plan.proto
|
||||
|
|
@ -110,6 +111,7 @@ set(PROTO_FILES
|
|||
event_list_rooms.proto
|
||||
event_move_card.proto
|
||||
event_notify_user.proto
|
||||
event_player_order_changed.proto
|
||||
event_player_properties_changed.proto
|
||||
event_remove_from_list.proto
|
||||
event_remove_messages.proto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
syntax = "proto2";
|
||||
import "game_commands.proto";
|
||||
message Command_SetPlayerOrder {
|
||||
extend GameCommand {
|
||||
optional Command_SetPlayerOrder ext = 1206;
|
||||
}
|
||||
|
||||
// The desired seating order, expressed as player names. Only used when
|
||||
// randomize is false.
|
||||
repeated string player_names = 1;
|
||||
|
||||
// When set, the server shuffles the seating order instead of using
|
||||
// player_names.
|
||||
optional bool randomize = 2;
|
||||
}
|
||||
|
|
@ -24,4 +24,9 @@ message Event_GameStateChanged {
|
|||
|
||||
// the amount of seconds since the game started
|
||||
optional uint32 seconds_elapsed = 5;
|
||||
|
||||
// the id of the player this event is addressed to. Used to re-identify the
|
||||
// local player after seats have been shuffled. Only set for events sent to
|
||||
// a specific participant, never for omniscient/replay events.
|
||||
optional sint32 local_player_id = 6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto2";
|
||||
import "game_event.proto";
|
||||
message Event_PlayerOrderChanged {
|
||||
extend GameEvent {
|
||||
optional Event_PlayerOrderChanged ext = 2023;
|
||||
}
|
||||
|
||||
// The new seating order, expressed as player names.
|
||||
repeated string player_names = 1;
|
||||
|
||||
// Whether the order was obtained by randomizing rather than set explicitly.
|
||||
optional bool randomized = 2;
|
||||
}
|
||||
|
|
@ -180,6 +180,11 @@ message GameCommand {
|
|||
/// Server: Server_Player::cmdSetPlaymat
|
||||
/// Client: reflected via player properties changed event
|
||||
SET_PLAYMAT = 1035;
|
||||
|
||||
/// Set the seating (turn) order of the players.
|
||||
/// Server: Server_AbstractParticipant::cmdSetPlayerOrder
|
||||
/// Client: reflected via Event_PlayerOrderChanged
|
||||
SET_PLAYER_ORDER = 1206;
|
||||
}
|
||||
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ message GameEvent {
|
|||
CHANGE_ZONE_PROPERTIES = 2020;
|
||||
REVERSE_TURN = 2021;
|
||||
GAME_LOG_NOTICE = 2022;
|
||||
PLAYER_ORDER_CHANGED = 2023;
|
||||
}
|
||||
optional sint32 player_id = 1 [default = -1];
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ message Command_CreateGame {
|
|||
|
||||
// share decklists with all players when selected
|
||||
optional bool share_decklists_on_load = 14;
|
||||
|
||||
// shuffle the player order (seats) before each game starts
|
||||
optional bool shuffle_players = 15;
|
||||
}
|
||||
|
||||
message Command_JoinGame {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ message ServerInfo_Game {
|
|||
// decklists are sent to all players when loaded
|
||||
optional bool share_decklists_on_load = 15;
|
||||
|
||||
// players are shuffled into new seats before each game starts
|
||||
optional bool shuffle_players = 16;
|
||||
|
||||
// the current player count
|
||||
optional uint32 player_count = 30;
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ bool GameSettings::getShareDecklistsOnLoad() const
|
|||
return getValue("shareDecklistsOnLoad", "game").toBool();
|
||||
}
|
||||
|
||||
bool GameSettings::getShufflePlayers() const
|
||||
{
|
||||
return getValue("shuffleplayers", "game").toBool();
|
||||
}
|
||||
|
||||
bool GameSettings::getRememberGameSettings() const
|
||||
{
|
||||
return getValue("rememberGameSettings", "game", QString(), true).toBool();
|
||||
|
|
@ -151,6 +156,11 @@ void GameSettings::setShareDecklistsOnLoad(bool _shareDecklistsOnLoad)
|
|||
setValue(_shareDecklistsOnLoad, "shareDecklistsOnLoad", "game");
|
||||
}
|
||||
|
||||
void GameSettings::setShufflePlayers(bool _shufflePlayers)
|
||||
{
|
||||
setValue(_shufflePlayers, "shuffleplayers", "game");
|
||||
}
|
||||
|
||||
void GameSettings::setRememberGameSettings(bool _rememberGameSettings)
|
||||
{
|
||||
setValue(_rememberGameSettings, "rememberGameSettings", "game");
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public:
|
|||
[[nodiscard]] bool getCreateGameAsSpectator() const override;
|
||||
[[nodiscard]] int getDefaultStartingLifeTotal() const override;
|
||||
[[nodiscard]] bool getShareDecklistsOnLoad() const override;
|
||||
[[nodiscard]] bool getShufflePlayers() const override;
|
||||
[[nodiscard]] bool getRememberGameSettings() const override;
|
||||
[[nodiscard]] bool getLocalGameRememberSettings() const override;
|
||||
[[nodiscard]] int getLocalGameMaxPlayers() const override;
|
||||
|
|
@ -44,6 +45,7 @@ public:
|
|||
void setCreateGameAsSpectator(bool _createGameAsSpectator);
|
||||
void setDefaultStartingLifeTotal(int _defaultStartingLifeTotal);
|
||||
void setShareDecklistsOnLoad(bool _shareDecklistsOnLoad);
|
||||
void setShufflePlayers(bool _shufflePlayers);
|
||||
void setRememberGameSettings(bool _rememberGameSettings);
|
||||
void setLocalGameRememberSettings(bool value);
|
||||
void setLocalGameMaxPlayers(int value);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
268
tests/movecard_tests/player_order_test.cpp
Normal file
268
tests/movecard_tests/player_order_test.cpp
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#include "game/game_config.h"
|
||||
#include "game/role_info.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 <gtest/gtest.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_event.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/session_event.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
|
||||
RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server
|
||||
|
||||
class ScriptedRng : public RNG_Abstract
|
||||
{
|
||||
public:
|
||||
QList<unsigned int> values;
|
||||
|
||||
unsigned int rand(int min, int max) override
|
||||
{
|
||||
const unsigned int value = values.takeFirst();
|
||||
EXPECT_GE(value, static_cast<unsigned int>(min));
|
||||
EXPECT_LE(value, static_cast<unsigned int>(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 (int id : game.getSeatOrder()) {
|
||||
const auto *participant = game.getParticipants().value(id, nullptr);
|
||||
if (participant) {
|
||||
names.append(QString::fromStdString(participant->getUserInfo()->name()));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
QList<int> participantIdsInOrder(const Server_Game &game)
|
||||
{
|
||||
return game.getSeatOrder();
|
||||
}
|
||||
} // 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<int>(),
|
||||
.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, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"A", "B", "C"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({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<int>(),
|
||||
.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, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
game.reorderPlayerSeats({"C", "A", "B"});
|
||||
|
||||
// player ids are stable; only the seat order changes
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({2, 0, 1}));
|
||||
EXPECT_EQ(game.getParticipants().size(), 3);
|
||||
|
||||
// the host keeps its seat: creator A is still in the game, so host stays 0
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
|
||||
// no re-keying means every mapping keyed by player id stays valid
|
||||
const int gameId = game.getGameId();
|
||||
EXPECT_EQ(uiA.getGames().value(gameId).second, 0);
|
||||
EXPECT_EQ(uiB.getGames().value(gameId).second, 1);
|
||||
EXPECT_EQ(uiC.getGames().value(gameId).second, 2);
|
||||
}
|
||||
|
||||
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<int>(),
|
||||
.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, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, 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(participantIdsInOrder(game), QList<int>({2, 0, 1}));
|
||||
EXPECT_EQ(game.getPlayerCount(), 3);
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
}
|
||||
|
||||
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<int>(),
|
||||
.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, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, 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"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({1, 0, 2}));
|
||||
QSet<int> idSet(participantIdsInOrder(game).cbegin(), participantIdsInOrder(game).cend());
|
||||
EXPECT_EQ(idSet.size(), 3); // ids are unique
|
||||
// ids are stable, so the host (creator A, id 0) is unchanged
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue