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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue