diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index bc68d4d7c..03930b0de 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -158,6 +159,9 @@ void GameEventHandler::processGameEventContainer(const GameEventContainer &cont, case GameEvent::REVERSE_TURN: eventReverseTurn(event.GetExtension(Event_ReverseTurn::ext), playerId, context); break; + case GameEvent::TOURNAMENT_STATE: + emit tournamentStateChanged(event.GetExtension(Event_TournamentState::ext)); + break; default: { PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0); @@ -263,6 +267,12 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event int /*eventPlayerId*/, const GameEventContext & /*context*/) { + // Sub-games of a tournament report their parent hub game so the client can + // route "close game" back to the parent tab. + if (event.parent_game_id() != -1) { + game->getGameMetaInfo()->setParentGameId(event.parent_game_id()); + } + const int playerListSize = event.player_list_size(); QVector>> opponentDecksToDisplay; diff --git a/cockatrice/src/game/game_event_handler.h b/cockatrice/src/game/game_event_handler.h index 99277abb7..7914a3228 100644 --- a/cockatrice/src/game/game_event_handler.h +++ b/cockatrice/src/game/game_event_handler.h @@ -43,6 +43,7 @@ class Event_SetActivePhase; class Event_GameSay; class Event_Kicked; class Event_ReverseTurn; +class Event_TournamentState; class Event_Ping; inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler"); @@ -329,6 +330,7 @@ signals: void gameStopped(); void gameClosed(); + void tournamentStateChanged(const Event_TournamentState &state); void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId); void playerJoined(const ServerInfo_PlayerProperties &playerInfo); void playerLeft(int leavingPlayerId); diff --git a/cockatrice/src/game/game_meta_info.h b/cockatrice/src/game/game_meta_info.h index cdba1605f..8585238bd 100644 --- a/cockatrice/src/game/game_meta_info.h +++ b/cockatrice/src/game/game_meta_info.h @@ -84,6 +84,26 @@ public: return roomGameTypes.find(gameInfo_.game_types(index)).value(); } + bool isTournament() const + { + return gameInfo_.is_tournament(); + } + + void setIsTournament(bool t) + { + gameInfo_.set_is_tournament(t); + } + + int parentGameId() const + { + return parentGameId_; + } + + void setParentGameId(int id) + { + parentGameId_ = id; + } + public slots: void setStarted(bool s) { @@ -108,6 +128,7 @@ signals: private: ServerInfo_Game gameInfo_; + int parentGameId_ = -1; }; #endif // GAME_META_INFO_H diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 48ad85e88..1e9814ddb 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -90,10 +90,10 @@ public: * @param context Additional context (undo, judge, etc.). * @param options Processing options (UI suppression, reveal behavior). */ - void processGameEvent(GameEvent::GameEventType type, - const GameEvent &event, - const GameEventContext &context, - EventProcessingOptions options); + virtual void processGameEvent(GameEvent::GameEventType type, + const GameEvent &event, + const GameEventContext &context, + EventProcessingOptions options); /** @} */ @@ -266,7 +266,7 @@ signals: void cardZoneChanged(CardItem *card, bool sameZone); void requestCardMenuUpdate(const CardItem *card); -private: +protected: /** Owning player instance. */ PlayerLogic *player; diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 45ba09aac..084f3df30 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -37,6 +37,19 @@ PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool initializeZones(); } +PlayerLogic::PlayerLogic(const ServerInfo_User &info, + int _id, + bool _local, + bool _judge, + AbstractGame *_parent, + PlayerEventHandler *customEventHandler) + : QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)), + playerEventHandler(customEventHandler), playerActions(new PlayerActions(this)), active(false), conceded(false), + zoneId(0), dialogSemaphore(false) +{ + initializeZones(); +} + void PlayerLogic::initializeZones() { addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this)); diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index 6923b3afe..26cd63f50 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -99,7 +99,30 @@ public: PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent); ~PlayerLogic() override; - void initializeZones(); +protected: + /** + * @brief Constructor for subclasses that need a custom event handler (e.g. DraftPlayerLogic). + * + * @p customEventHandler must be non-null and either QObject-parented to this PlayerLogic + * or deleted externally; it is not owned by PlayerLogic. The handler connects to @c player + * during its own constructor, so passing a freshly built subclass handler from an + * initializer list is safe. + */ + PlayerLogic(const ServerInfo_User &info, + int _id, + bool _local, + bool _judge, + AbstractGame *_parent, + PlayerEventHandler *customEventHandler); + +public: + /** + * @brief Creates the standard zone set. + * + * Not virtually dispatched from constructors — subclasses overriding this must add + * their extra zones in their own constructor body. + */ + virtual void initializeZones(); void updateZones(); void clear(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index e11a962d1..bd9ec0c0c 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -14,9 +14,12 @@ set(HEADERS game/server_deck_validation_strategy.h game/server_game.h game/server_game_lifecycle_strategy.h - game/server_match_result_strategy.h game/server_match_game_factory.h + game/server_match_result_strategy.h game/server_player.h + game/server_tournament.h + game/server_tournament_lifecycle_strategy.h + game/server_tournament_match_result_strategy.h game/server_spectator.h server.h server_abstractuserinterface.h @@ -43,6 +46,9 @@ add_library( game/server_game.cpp game/server_player.cpp game/server_spectator.cpp + game/server_tournament.cpp + game/server_tournament_lifecycle_strategy.cpp + game/server_tournament_match_result_strategy.cpp server.cpp server_abstractuserinterface.cpp server_database_interface.cpp diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index a6280ffa1..ea0b36ea5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -536,6 +537,15 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo case GameCommand::SET_PLAYMAT: return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges); break; + case GameCommand::REPORT_MATCH_RESULT: + return cmdReportMatchResult(command.GetExtension(Command_ReportMatchResult::ext), rc, ges); + break; + case GameCommand::ADVANCE_TOURNAMENT: + return cmdAdvanceTournament(command.GetExtension(Command_AdvanceTournament::ext), rc, ges); + break; + case GameCommand::TOURNAMENT_SETTINGS_SELECT: + return cmdTournamentSettingsSelect(command.GetExtension(Command_TournamentSettingsSelect::ext), rc, ges); + break; default: return Response::RespInvalidCommand; } @@ -570,7 +580,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface * void Server_AbstractParticipant::disconnectClient() { bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered; - if (!isRegistered || spectator) { + if (!isRegistered || spectator || game->getDisconnectRemovesPlayer()) { game->removeParticipant(this, Event_Leave::USER_DISCONNECTED); } else { setUserInterface(nullptr); @@ -584,3 +594,25 @@ void Server_AbstractParticipant::getInfo(ServerInfo_Player *info, { getProperties(*info->mutable_properties(), withUserInfo); } + +Response::ResponseCode Server_AbstractParticipant::cmdReportMatchResult(const Command_ReportMatchResult & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespContextError; +} + +Response::ResponseCode Server_AbstractParticipant::cmdAdvanceTournament(const Command_AdvanceTournament & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespContextError; +} + +Response::ResponseCode +Server_AbstractParticipant::cmdTournamentSettingsSelect(const Command_TournamentSettingsSelect & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespContextError; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index c78ae78c1..b41ecbb20 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -53,6 +53,9 @@ class Command_DeckSelect; class Command_SetSideboardLock; class Command_ChangeZoneProperties; class Command_SetPlaymat; +class Command_ReportMatchResult; +class Command_AdvanceTournament; +class Command_TournamentSettingsSelect; class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container { @@ -175,6 +178,13 @@ public: cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdReportMatchResult(const Command_ReportMatchResult &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdAdvanceTournament(const Command_AdvanceTournament &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdTournamentSettingsSelect(const Command_TournamentSettingsSelect &cmd, + ResponseContainer &rc, + GameEventStorage &ges); Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); void sendGameEvent(const GameEventContainer &event); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 957a89792..a9d486a4d 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -1663,3 +1663,9 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res playmatParams->set_zoom(playmat.params.zoom); } } + +void Server_AbstractPlayer::setDeck(DeckList *_deck) +{ + delete deck; + deck = _deck; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index 85fbc0557..312f62d0e 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -47,6 +47,7 @@ public: { return deck; } + void setDeck(DeckList *_deck); bool getReadyStart() const { return readyStart; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 43209e994..d5d552b31 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -30,10 +30,14 @@ #include "server_cardzone.h" #include "server_player.h" #include "server_spectator.h" +#include "server_tournament.h" +#include "server_tournament_lifecycle_strategy.h" +#include "server_tournament_match_result_strategy.h" #include #include #include +#include #include #include #include @@ -62,8 +66,9 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) 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), - deckValidationStrategy(new Server_DefaultDeckValidationStrategy), + turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), isTournament(false), + tournament(nullptr), tournamentParentGame(nullptr), tournamentMatchPlayer1Id(-1), tournamentMatchPlayer2Id(-1), + disconnectRemovesPlayer(false), deckValidationStrategy(new Server_DefaultDeckValidationStrategy), lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy), gameMutex() { @@ -265,6 +270,12 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, event->set_game_started(false); } + event->set_is_tournament(isTournament); + + if (tournamentParentGame) { + event->set_parent_game_id(tournamentParentGame->getGameId()); + } + for (Server_AbstractParticipant *participant : participants.values()) { participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo); } @@ -313,7 +324,7 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); QMutexLocker locker(&gameMutex); - if (getPlayerCount() < maxPlayers && !forceStartGame) { + if (!isTournament && getPlayerCount() < maxPlayers && !forceStartGame) { return; } @@ -861,6 +872,7 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_share_decklists_on_load(shareDecklistsOnLoad); result.set_spectators_count(getSpectatorCount()); result.set_start_time(startTime.toSecsSinceEpoch()); + result.set_is_tournament(isTournament); } } @@ -911,3 +923,86 @@ void Server_Game::setDeckValidationStrategy(Server_DeckValidationStrategy *strat { deckValidationStrategy.reset(strategy); } + +void Server_Game::setMatchResultStrategy(Server_MatchResultStrategy *strategy) +{ + matchResultStrategy.reset(strategy); +} + +void Server_Game::setIsTournamentGame(bool _isTournament) +{ + isTournament = _isTournament; + if (isTournament) { + tournament = new Server_Tournament(this, this, this); + lifecycleStrategy.reset(new Server_TournamentLifecycleStrategy); + matchResultStrategy.reset(new Server_TournamentMatchResultStrategy); + } else if (tournament) { + delete tournament; + tournament = nullptr; + lifecycleStrategy.reset(new Server_DefaultLifecycleStrategy); + matchResultStrategy.reset(new Server_NullMatchResultStrategy); + } +} + +void Server_Game::startTournament() +{ + if (!tournament) { + tournament = new Server_Tournament(this, this, this); + } + + if (!tournament->isStarted()) { + // Add all current players to the tournament + auto players = getPlayers(); + for (auto *player : players.values()) { + tournament->addPlayer(player->getPlayerId(), QString::fromStdString(player->getUserInfo()->name())); + } + + tournament->startTournament(); + } + + GameEventStorage ges; + tournament->broadcastTournamentState(ges); + ges.sendToGame(this); +} + +void Server_Game::setPlayerTournamentDeck(int playerId, DeckList *deck) +{ + if (tournament) { + tournament->setPlayerDeck(playerId, deck); + } +} + +void Server_Game::setTournamentMatchInfo(Server_Game *parentGame, int p1Id, int p2Id) +{ + tournamentParentGame = parentGame; + tournamentMatchPlayer1Id = p1Id; + tournamentMatchPlayer2Id = p2Id; +} + +Server_Game *Server_Game::createMatchGame(const GameConfig &config, int &outGameId) +{ + Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); + outGameId = databaseInterface->getNextGameId(); + if (outGameId == -1) { + return nullptr; + } + + GameConfig matchConfig = config; + matchConfig.gameId = outGameId; + auto *game = new Server_Game(matchConfig, room); + // Sub-games carry the tournament flag (for protocol fields) but keep the default + // strategies; the parent tournament drives them through the match result strategy + // installed by Server_Tournament::createMatchGame. + game->isTournament = true; + return game; +} + +Server_AbstractUserInterface *Server_Game::getUserInterface(const QString &playerName) +{ + return room->getUserInterfaceByName(playerName); +} + +void Server_Game::addGameToRoom(Server_Game *game) +{ + room->addGame(game); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 1b9f651bd..3e655839d 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -24,6 +24,7 @@ #include "game_config.h" #include "server_deck_validation_strategy.h" #include "server_game_lifecycle_strategy.h" +#include "server_match_game_factory.h" #include "server_match_result_strategy.h" #include @@ -34,21 +35,26 @@ #include #include #include +#include #include #include class QTimer; +class DeckList; class GameEventContainer; +class GameEventStorage; class GameReplay; class Server_Room; class Server_AbstractPlayer; class Server_AbstractParticipant; +class Server_Card; +class Server_Tournament; class ServerInfo_User; class ServerInfo_Game; class Server_AbstractUserInterface; class Event_GameStateChanged; -class Server_Game : public QObject +class Server_Game : public QObject, public Server_MatchGameFactory { Q_OBJECT private: @@ -83,6 +89,14 @@ private: QList replayList; GameReplay *currentReplay; + bool isTournament; + TournamentSettings tournamentSettings; + Server_Tournament *tournament; + Server_Game *tournamentParentGame; + int tournamentMatchPlayer1Id; + int tournamentMatchPlayer2Id; + bool disconnectRemovesPlayer; + QScopedPointer deckValidationStrategy; QScopedPointer lifecycleStrategy; @@ -220,6 +234,45 @@ public: void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); /** @brief Get the current deck validation strategy (non-owning). */ + bool getIsTournamentGame() const + { + return isTournament; + } + void setIsTournamentGame(bool _isTournament); + bool getIsTournament() const + { + return tournament != nullptr; + } + Server_Tournament *getTournament() const + { + return tournament; + } + void startTournament(); + void setPlayerTournamentDeck(int playerId, DeckList *deck); + void setTournamentMatchInfo(Server_Game *parentGame, int p1Id, int p2Id); + Server_Game *getTournamentParentGame() const + { + return tournamentParentGame; + } + bool getDisconnectRemovesPlayer() const + { + return disconnectRemovesPlayer; + } + + // Server_MatchGameFactory implementation + Server_Game *createMatchGame(const GameConfig &config, int &outGameId) override; + Server_AbstractUserInterface *getUserInterface(const QString &playerName) override; + void addGameToRoom(Server_Game *game) override; + + const TournamentSettings &getTournamentSettings() const + { + return tournamentSettings; + } + void setTournamentSettings(const TournamentSettings &settings) + { + tournamentSettings = settings; + } + Server_DeckValidationStrategy *getDeckValidationStrategy() const { return deckValidationStrategy.data(); @@ -232,6 +285,8 @@ public: { return lifecycleStrategy.data(); } + /** @brief Replace the match result strategy; takes ownership of @p strategy. */ + void setMatchResultStrategy(Server_MatchResultStrategy *strategy); }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.cpp new file mode 100644 index 000000000..1d13f31f7 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.cpp @@ -0,0 +1,556 @@ +#include "server_tournament.h" + +#include "../server_abstractuserinterface.h" +#include "../server_response_containers.h" +#include "../serverinfo_user_container.h" +#include "game_config.h" +#include "server_abstract_player.h" +#include "server_game.h" +#include "server_match_game_factory.h" +#include "server_player.h" +#include "server_tournament_match_result_strategy.h" + +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(TournamentLog, "tournament"); + +Server_Tournament::Server_Tournament(Server_Game *_parentGame, Server_MatchGameFactory *_factory, QObject *parent) + : QObject(parent), parentGame(_parentGame), matchGameFactory(_factory), currentRound(0), totalRounds(0), + started(false) +{ +} + +Server_Tournament::~Server_Tournament() +{ + qDeleteAll(submittedDecks); +} + +void Server_Tournament::addPlayer(int playerId, const QString &playerName) +{ + TournamentPlayerData data; + data.playerId = playerId; + data.playerName = playerName; + players[playerId] = data; +} + +void Server_Tournament::setPlayerDeck(int playerId, DeckList *deck) +{ + QMutexLocker locker(&tournamentMutex); + delete submittedDecks.value(playerId, nullptr); + submittedDecks[playerId] = deck; + if (players.contains(playerId)) { + players[playerId].deckSubmitted = true; + } +} + +void Server_Tournament::removePlayer(int playerId) +{ + QMutexLocker locker(&tournamentMutex); + players.remove(playerId); +} + +void Server_Tournament::startTournament() +{ + { + QMutexLocker locker(&tournamentMutex); + if (started) { + return; + } + totalRounds = calculateTotalRounds(); + started = true; + currentRound = 0; + generateSwissPairings(); + } + + // Spawn the first round's match games + enqueueMatchGameCreation(); +} + +bool Server_Tournament::isAllDecksSubmitted() const +{ + QMutexLocker locker(&tournamentMutex); + for (auto it = players.constBegin(); it != players.constEnd(); ++it) { + if (!it->deckSubmitted) { + return false; + } + } + return true; +} + +int Server_Tournament::getTournamentPlayerIdByName(const QString &name) const +{ + QMutexLocker locker(&tournamentMutex); + for (auto it = players.constBegin(); it != players.constEnd(); ++it) { + if (it->playerName == name) { + return it->playerId; + } + } + return -1; +} + +void Server_Tournament::generateSwissPairings() +{ + currentPairings.clear(); + QList available; + for (auto it = players.constBegin(); it != players.constEnd(); ++it) { + available.append(it->playerId); + } + + // Sort by wins descending (and by record for tie-breaking) + std::sort(available.begin(), available.end(), [this](int a, int b) { + const auto &pa = players[a]; + const auto &pb = players[b]; + if (pa.wins != pb.wins) { + return pa.wins > pb.wins; + } + if (pa.losses != pb.losses) { + return pa.losses < pb.losses; + } + return a < b; + }); + + // Simple greedy Swiss pairing + QSet paired; + for (int i = 0; i < available.size(); ++i) { + if (paired.contains(available[i])) { + continue; + } + for (int j = i + 1; j < available.size(); ++j) { + if (paired.contains(available[j])) { + continue; + } + if (!havePlayed(available[i], available[j])) { + TournamentPairingData pairing; + pairing.player1Id = available[i]; + pairing.player2Id = available[j]; + currentPairings.append(pairing); + paired.insert(available[i]); + paired.insert(available[j]); + break; + } + } + } + + // Bye for unpaired player if odd count + for (int i = 0; i < available.size(); ++i) { + if (!paired.contains(available[i])) { + // Player gets a bye (auto-win) + TournamentPairingData bye; + bye.player1Id = available[i]; + bye.player2Id = -1; + bye.winnerId = available[i]; + bye.player1MatchWins = gamesPerMatch; // Match immediately decided + currentPairings.append(bye); + players[available[i]].wins += 1; + allPreviousPairings.append(qMakePair(available[i], -1)); + break; + } + } +} + +int Server_Tournament::calculateTotalRounds() const +{ + int n = players.size(); + if (n <= 1) { + return 0; + } + // Standard Swiss rounds: ceil(log2(n)) + int rounds = 0; + while ((1 << rounds) < n) { + ++rounds; + } + return rounds; +} + +bool Server_Tournament::havePlayed(int p1, int p2) const +{ + for (const auto &pair : allPreviousPairings) { + if ((pair.first == p1 && pair.second == p2) || (pair.first == p2 && pair.second == p1)) { + return true; + } + } + return false; +} + +void Server_Tournament::advanceRound(GameEventStorage &ges) +{ + { + QMutexLocker locker(&tournamentMutex); + ++currentRound; + if (currentRound >= totalRounds) { + broadcastTournamentState(ges); + return; + } + generateSwissPairings(); + } + + enqueueMatchGameCreation(); + broadcastTournamentState(ges); +} + +bool Server_Tournament::allPairingsDecided() const +{ + for (const auto &pairing : currentPairings) { + if (pairing.winnerId == -2) { + return false; + } + } + return true; +} + +void Server_Tournament::enqueueMatchGameCreation() +{ + QList> planned; + { + QMutexLocker locker(&tournamentMutex); + for (const auto &pairing : currentPairings) { + if (pairing.player2Id != -1 && pairing.winnerId == -2 && + pairing.matchGameIds.size() < static_cast(gamesPerMatch)) { + planned.append(qMakePair(pairing.player1Id, pairing.player2Id)); + } + } + } + if (planned.isEmpty()) { + return; + } + + // Create the games from the event loop instead of the caller's stack: command + // processing holds game mutexes, and room registration takes gamesLock, so spawning + // synchronously would nest lock orders. The queued job runs once this object's + // owning thread returns to its event loop with no locks held; it is dropped if this + // tournament is destroyed first. + QMetaObject::invokeMethod( + this, + [this, planned] { + for (const auto &pair : planned) { + createMatchGame(pair.first, pair.second); + } + + GameEventStorage ges; + broadcastTournamentState(ges); + ges.sendToGame(parentGame); + }, + Qt::QueuedConnection); +} + +void Server_Tournament::createMatchGame(int player1Id, int player2Id) +{ + if (!matchGameFactory || player2Id == -1) { + return; + } + + QString player1Name; + QString player2Name; + QString deck1Native; + QString deck2Native; + int round = 0; + int gameNumber = 1; + { + QMutexLocker locker(&tournamentMutex); + player1Name = players.value(player1Id).playerName; + player2Name = players.value(player2Id).playerName; + + if (submittedDecks.contains(player1Id)) { + deck1Native = submittedDecks.value(player1Id)->writeToString_Native(); + } + if (submittedDecks.contains(player2Id)) { + deck2Native = submittedDecks.value(player2Id)->writeToString_Native(); + } + + round = currentRound; + for (const auto &pairing : currentPairings) { + if (pairing.player1Id == player1Id && pairing.player2Id == player2Id) { + gameNumber = pairing.matchGameIds.size() + 1; + break; + } + } + + // Defense in depth: never exceed the configured series length + for (const auto &pairing : currentPairings) { + if (pairing.player1Id == player1Id && pairing.player2Id == player2Id && + pairing.matchGameIds.size() >= static_cast(gamesPerMatch)) { + qCWarning(TournamentLog) << "Refusing to exceed series length for pairing" << player1Id << player2Id; + return; + } + } + } + + // Create a sub-game for this match via the factory + ServerInfo_User creatorInfo; + creatorInfo.set_name(player1Name.toStdString()); + creatorInfo.set_user_level(ServerInfo_User::IsAdmin | ServerInfo_User::IsRegistered); + + QString gameDesc = gamesPerMatch > 1 + ? QString("R%1 Match - Game %2 of %3").arg(round).arg(gameNumber).arg(gamesPerMatch) + : QString("Tournament Round %1").arg(round); + + GameConfig matchConfig; + matchConfig.creatorInfo = creatorInfo; + matchConfig.description = gameDesc; + matchConfig.maxPlayers = 2; + matchConfig.startingLifeTotal = parentGame->getStartingLifeTotal(); + + int matchGameId = -1; + auto *matchGame = matchGameFactory->createMatchGame(matchConfig, matchGameId); + if (!matchGame || matchGameId == -1) { + return; + } + + matchGame->setTournamentMatchInfo(parentGame, player1Id, player2Id); + matchGame->setMatchResultStrategy(new Server_TournamentMatchResultStrategy); + matchGameFactory->addGameToRoom(matchGame); + + // Store the game ID in the pairing + { + QMutexLocker locker(&tournamentMutex); + for (auto &pairing : currentPairings) { + if (pairing.player1Id == player1Id && pairing.player2Id == player2Id) { + pairing.gameId = matchGameId; + pairing.matchGameIds.append(matchGameId); + break; + } + } + } + + // Auto-join both players, sending the join event directly through their UIs. + QMap> joiners; + + auto joinAndSetupPlayer = [&](int pid, const QString &name) { + Server_AbstractUserInterface *ui = matchGameFactory->getUserInterface(name); + if (ui) { + auto *rc = new ResponseContainer(0); + matchGame->addPlayer(ui, *rc, false, false, false); + joiners[pid] = qMakePair(ui, rc); + } + }; + + joinAndSetupPlayer(player1Id, player1Name); + joinAndSetupPlayer(player2Id, player2Name); + + // Now send the enqueued GameJoined + GameStateChanged events to each player's client. + for (auto it = joiners.constBegin(); it != joiners.constEnd(); ++it) { + it.value().first->sendResponseContainer(*it.value().second, Response::RespNothing); + delete it.value().second; + } + joiners.clear(); + + // Set decks and mark players as ready in the match game + auto matchPlayers = matchGame->getPlayers(); + for (auto *matchPlayer : matchPlayers) { + const QString name = QString::fromStdString(matchPlayer->getUserInfo()->name()); + QString deckNative; + if (name == player1Name) { + deckNative = deck1Native; + } else if (name == player2Name) { + deckNative = deck2Native; + } + + if (!deckNative.isEmpty()) { + matchPlayer->setDeck(new DeckList(deckNative)); + matchPlayer->setReadyStart(true); + } + } + + // Start the match game + matchGame->startGameIfReady(true); +} + +void Server_Tournament::recordMatchResult(int playerId1, int playerId2, int winnerId, GameEventStorage &ges) +{ + QMutexLocker locker(&tournamentMutex); + + // Find the pairing and set the winner + for (auto &pairing : currentPairings) { + if ((pairing.player1Id == playerId1 && pairing.player2Id == playerId2) || + (pairing.player1Id == playerId2 && pairing.player2Id == playerId1)) { + if (pairing.winnerId != -2) { + return; // Already recorded — defense in depth against double-call + } + pairing.winnerId = winnerId; + break; + } + } + + // Update player records + if (winnerId == -1) { + // Draw + players[playerId1].draws += 1; + players[playerId2].draws += 1; + } else if (winnerId == playerId1) { + players[playerId1].wins += 1; + players[playerId2].losses += 1; + } else if (winnerId == playerId2) { + players[playerId2].wins += 1; + players[playerId1].losses += 1; + } + + // Store for future pairing avoidance + allPreviousPairings.append(qMakePair(playerId1, playerId2)); + + // Check if all pairings in current round have results + bool allDecided = true; + for (const auto &pairing : currentPairings) { + if (pairing.winnerId == -2) { + allDecided = false; + break; + } + } + + broadcastTournamentState(ges); + + if (allDecided) { + advanceRound(ges); + } +} + +bool Server_Tournament::recordMatchResultByGameId(int gameId, int winnerId, GameEventStorage &ges) +{ + bool matchDecided = false; + bool seriesContinues = false; + int p1 = -1; + int p2 = -1; + { + QMutexLocker locker(&tournamentMutex); + + // Find the pairing that owns this game + TournamentPairingData *pairingPtr = nullptr; + for (auto &pairing : currentPairings) { + if (pairing.matchGameIds.contains(gameId)) { + pairingPtr = &pairing; + break; + } + } + + if (!pairingPtr) { + return false; + } + + // If the match is already decided, ignore further sub-game results + if (pairingPtr->winnerId != -2) { + return true; + } + + // Increment per-match wins + if (winnerId == pairingPtr->player1Id) { + pairingPtr->player1MatchWins += 1; + } else if (winnerId == pairingPtr->player2Id) { + pairingPtr->player2MatchWins += 1; + } + // Draw (winnerId == -1): no match wins incremented + + // Check if match is decided + const int gamesNeeded = static_cast(gamesPerMatch); + matchDecided = (pairingPtr->player1MatchWins >= gamesNeeded) || (pairingPtr->player2MatchWins >= gamesNeeded); + + if (matchDecided) { + // Determine match winner + int matchWinnerId; + if (pairingPtr->player1MatchWins >= gamesNeeded) { + matchWinnerId = pairingPtr->player1Id; + } else { + matchWinnerId = pairingPtr->player2Id; + } + + // Set the match winner on the pairing + pairingPtr->winnerId = matchWinnerId; + + // Update tournament-level player records + if (matchWinnerId == pairingPtr->player1Id) { + players[pairingPtr->player1Id].wins += 1; + players[pairingPtr->player2Id].losses += 1; + } else { + players[pairingPtr->player2Id].wins += 1; + players[pairingPtr->player1Id].losses += 1; + } + + // Store for future pairing avoidance + allPreviousPairings.append(qMakePair(pairingPtr->player1Id, pairingPtr->player2Id)); + } else { + // Match not decided — spawn the next sub-game outside all locks + seriesContinues = true; + p1 = pairingPtr->player1Id; + p2 = pairingPtr->player2Id; + } + + broadcastTournamentState(ges); + } + + if (seriesContinues) { + QMetaObject::invokeMethod( + this, + [this, p1, p2] { + createMatchGame(p1, p2); + + GameEventStorage nextGes; + broadcastTournamentState(nextGes); + nextGes.sendToGame(parentGame); + }, + Qt::QueuedConnection); + } + + checkAndAdvanceRound(ges); + + return matchDecided; +} + +void Server_Tournament::checkAndAdvanceRound(GameEventStorage &ges) +{ + bool roundComplete = false; + { + QMutexLocker locker(&tournamentMutex); + roundComplete = allPairingsDecided(); + } + + if (roundComplete) { + advanceRound(ges); + } +} + +void Server_Tournament::broadcastTournamentState(GameEventStorage &ges) +{ + QMutexLocker locker(&tournamentMutex); + + Event_TournamentState state; + + if (started && currentRound >= totalRounds) { + state.set_phase(Event_TournamentState::PHASE_FINISHED); + } else if (started) { + state.set_phase(Event_TournamentState::PHASE_PLAYING); + } else { + state.set_phase(Event_TournamentState::PHASE_DECK_BUILDING); + } + + state.set_current_round(currentRound); + state.set_total_rounds(totalRounds); + + // Settings + TournamentSettings *settings = state.mutable_settings(); + settings->set_games_per_match(gamesPerMatch); + + for (auto it = players.constBegin(); it != players.constEnd(); ++it) { + TournamentPlayer *p = state.add_players(); + p->set_player_id(it->playerId); + p->set_player_name(it->playerName.toStdString()); + p->set_wins(it->wins); + p->set_losses(it->losses); + p->set_draws(it->draws); + p->set_deck_submitted(it->deckSubmitted); + } + + for (const auto &pairing : currentPairings) { + TournamentPairing *p = state.add_pairings(); + p->set_player1_id(pairing.player1Id); + p->set_player2_id(pairing.player2Id); + p->set_game_id(pairing.gameId); + // Map internal sentinel: -2 (undecided) -> -1 (no winner yet in proto) + p->set_winner_id(pairing.winnerId == -2 ? -1 : pairing.winnerId); + p->set_player1_match_wins(pairing.player1MatchWins); + p->set_player2_match_wins(pairing.player2MatchWins); + } + + ges.enqueueGameEvent(state, -1); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.h new file mode 100644 index 000000000..b59edda4f --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament.h @@ -0,0 +1,106 @@ +#ifndef SERVER_TOURNAMENT_H +#define SERVER_TOURNAMENT_H + +#include +#include +#include +#include +#include +#include + +class DeckList; +class Server_Game; +class Server_MatchGameFactory; +class Server_AbstractParticipant; +class Server_AbstractUserInterface; +class GameEventStorage; + +/** @brief Maximum number of games per match a tournament can be configured with. */ +constexpr int MAX_GAMES_PER_MATCH = 5; + +class Server_Tournament : public QObject +{ + Q_OBJECT +public: + explicit Server_Tournament(Server_Game *_parentGame, Server_MatchGameFactory *_factory, QObject *parent = nullptr); + ~Server_Tournament() override; + + void addPlayer(int playerId, const QString &playerName); + void removePlayer(int playerId); + void startTournament(); + void advanceRound(GameEventStorage &ges); + void recordMatchResult(int playerId1, int playerId2, int winnerId, GameEventStorage &ges); + bool recordMatchResultByGameId(int gameId, int winnerId, GameEventStorage &ges); + void broadcastTournamentState(GameEventStorage &ges); + + bool isStarted() const + { + return started; + } + bool isAllDecksSubmitted() const; + int getPlayerCount() const + { + return players.size(); + } + int getTournamentPlayerIdByName(const QString &name) const; + void setPlayerDeckSubmitted(int playerId) + { + if (players.contains(playerId)) { + players[playerId].deckSubmitted = true; + } + } + void setPlayerDeck(int playerId, DeckList *deck); + + void setGamesPerMatch(uint32_t n) + { + gamesPerMatch = n; + } + uint32_t getGamesPerMatch() const + { + return gamesPerMatch; + } + + struct TournamentPlayerData + { + int playerId; + QString playerName; + int wins = 0; + int losses = 0; + int draws = 0; + bool deckSubmitted = false; + }; + + struct TournamentPairingData + { + int player1Id; + int player2Id; + int gameId = -1; + int winnerId = -2; // -2 = undecided, -1 = draw, >= 0 = winner player id + int player1MatchWins = 0; + int player2MatchWins = 0; + QList matchGameIds; + }; + +private: + Server_Game *parentGame; + Server_MatchGameFactory *matchGameFactory; + mutable QRecursiveMutex tournamentMutex; + QMap players; + QMap submittedDecks; + QList currentPairings; + QList> allPreviousPairings; + int currentRound; + int totalRounds; + bool started; + uint32_t gamesPerMatch = 1; + + void generateSwissPairings(); + int calculateTotalRounds() const; + bool havePlayed(int p1, int p2) const; + bool allPairingsDecided() const; + void createMatchGame(int player1Id, int player2Id); + void enqueueMatchGameCreation(); + void checkAndAdvanceRound(GameEventStorage &ges); +}; + +#endif // SERVER_TOURNAMENT_H diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.cpp new file mode 100644 index 000000000..5149b9a26 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.cpp @@ -0,0 +1,33 @@ +#include "server_tournament_lifecycle_strategy.h" + +#include "server_abstract_player.h" +#include "server_game.h" + +#include + +inline Q_LOGGING_CATEGORY(TournamentLifecycleLog, "tournament_lifecycle"); + +Server_GameLifecycleStrategy::StartAction Server_TournamentLifecycleStrategy::onGameStarting(Server_Game *game) +{ + // Match sub-games start through the normal flow; only the tournament hub game is + // managed by this lifecycle. + if (game->getTournamentParentGame() != nullptr) { + return StartAction::ProceedNormal; + } + + for (auto *player : game->getPlayers().values()) { + if (!player->getDeckList()) { + qCWarning(TournamentLifecycleLog) + << "Tournament cannot start: player" << player->getUserInfo()->name().c_str() << "has no deck"; + return StartAction::Handled; + } + } + + if (!game->getIsTournamentGame()) { + qCWarning(TournamentLifecycleLog) << "Tournament lifecycle used for non-tournament game — falling back"; + return StartAction::ProceedNormal; + } + + game->startTournament(); + return StartAction::Handled; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.h new file mode 100644 index 000000000..c6dbede1b --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_lifecycle_strategy.h @@ -0,0 +1,12 @@ +#ifndef SERVER_TOURNAMENT_LIFECYCLE_STRATEGY_H +#define SERVER_TOURNAMENT_LIFECYCLE_STRATEGY_H + +#include "server_game_lifecycle_strategy.h" + +class Server_TournamentLifecycleStrategy : public Server_GameLifecycleStrategy +{ +public: + StartAction onGameStarting(Server_Game *game) override; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.cpp new file mode 100644 index 000000000..5611224e5 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.cpp @@ -0,0 +1,34 @@ +#include "server_tournament_match_result_strategy.h" + +#include "../server_response_containers.h" +#include "server_abstract_player.h" +#include "server_game.h" +#include "server_tournament.h" + +#include +#include + +bool Server_TournamentMatchResultStrategy::onGameFinished(Server_Game *game, + int playing, + Server_AbstractPlayer *lastPlayer) +{ + auto *parentGame = game->getTournamentParentGame(); + if (!parentGame || !parentGame->getTournament()) { + return false; + } + + int winnerId; + if (playing == 0) { + winnerId = -1; + } else { + QString winnerName = QString::fromStdString(lastPlayer->getUserInfo()->name()); + auto *tournament = parentGame->getTournament(); + winnerId = tournament->getTournamentPlayerIdByName(winnerName); + } + + GameEventStorage parentGes; + bool matchDecided = parentGame->getTournament()->recordMatchResultByGameId(game->getGameId(), winnerId, parentGes); + parentGes.sendToGame(parentGame); + + return matchDecided; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.h new file mode 100644 index 000000000..7bc3bafb8 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_tournament_match_result_strategy.h @@ -0,0 +1,12 @@ +#ifndef SERVER_TOURNAMENT_MATCH_RESULT_STRATEGY_H +#define SERVER_TOURNAMENT_MATCH_RESULT_STRATEGY_H + +#include "server_match_result_strategy.h" + +class Server_TournamentMatchResultStrategy : public Server_MatchResultStrategy +{ +public: + bool onGameFinished(Server_Game *game, int playing, Server_AbstractPlayer *lastPlayer) override; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 899df6529..a3802441f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -3,6 +3,7 @@ #include "game/game_config.h" #include "game/server_game.h" #include "game/server_player.h" +#include "game/server_tournament.h" #include "server_database_interface.h" #include "server_room.h" @@ -916,6 +917,8 @@ 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 isTournament = cmd.has_is_tournament() ? cmd.is_tournament() : false; + int gamesPerMatch = cmd.has_games_per_match() ? static_cast(cmd.games_per_match()) : 1; const int gameId = databaseInterface->getNextGameId(); if (gameId == -1) { @@ -940,6 +943,10 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room .shareDecklistsOnLoad = shareDecklistsOnLoad}; auto *game = new Server_Game(config, room); + game->setIsTournamentGame(isTournament); + if (isTournament && game->getTournament()) { + game->getTournament()->setGamesPerMatch(static_cast(qBound(1, gamesPerMatch, MAX_GAMES_PER_MATCH))); + } game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 1f29e62fb..f6880e9ff 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -363,6 +363,18 @@ void Server_Room::broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool sendRoomEvent(prepareRoomEvent(event), sendToIsl); } +Server_AbstractUserInterface *Server_Room::getUserInterfaceByName(const QString &name) const +{ + usersLock.lockForRead(); + auto it = users.constFind(name); + Server_AbstractUserInterface *result = nullptr; + if (it != users.constEnd()) { + result = it.value(); + } + usersLock.unlock(); + return result; +} + void Server_Room::addGame(Server_Game *game) { ServerInfo_Room roomInfo; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.h b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h index 3d9988f20..02b865ebc 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h @@ -135,6 +135,8 @@ public: void addGame(Server_Game *game); void removeGame(Server_Game *game); + Server_AbstractUserInterface *getUserInterfaceByName(const QString &name) const; + void sendRoomEvent(RoomEvent *event, bool sendToIsl = true); RoomEvent *prepareRoomEvent(const ::google::protobuf::Message &roomEvent); }; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 3a193ae3c..6708b06cb 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -60,6 +60,7 @@ set(PROTO_FILES command_set_sideboard_lock.proto command_set_sideboard_plan.proto command_shuffle.proto + command_tournament.proto command_undo_draw.proto commands.proto context_concede.proto @@ -118,6 +119,7 @@ set(PROTO_FILES event_set_card_counter.proto event_set_counter.proto event_shuffle.proto + event_tournament_state.proto event_user_joined.proto event_user_left.proto event_user_message.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_tournament.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_tournament.proto new file mode 100644 index 000000000..ee958f587 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_tournament.proto @@ -0,0 +1,26 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "event_tournament_state.proto"; + +message Command_ReportMatchResult { + extend GameCommand { + optional Command_ReportMatchResult ext = 1037; + } + + optional sint32 game_id = 1 [default = -1]; + optional sint32 winner_id = 2 [default = -1]; +} + +message Command_AdvanceTournament { + extend GameCommand { + optional Command_AdvanceTournament ext = 1038; + } +} + +message Command_TournamentSettingsSelect { + extend GameCommand { + optional Command_TournamentSettingsSelect ext = 1039; + } + + optional TournamentSettings settings = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto index 5c8aee3fe..c085e6a59 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto @@ -24,4 +24,10 @@ message Event_GameStateChanged { // the amount of seconds since the game started optional uint32 seconds_elapsed = 5; + + // whether this game is a tournament game + optional bool is_tournament = 9; + + // for tournament sub-games: the ID of the parent tournament game (-1 if not a sub-game) + optional sint32 parent_game_id = 10 [default = -1]; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_tournament_state.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_tournament_state.proto new file mode 100644 index 000000000..5fb003f50 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_tournament_state.proto @@ -0,0 +1,43 @@ +syntax = "proto2"; +import "game_event.proto"; + +message TournamentPlayer { + optional sint32 player_id = 1; + optional string player_name = 2; + optional uint32 wins = 3; + optional uint32 losses = 4; + optional uint32 draws = 5; + optional bool deck_submitted = 6; +} + +message TournamentPairing { + optional sint32 player1_id = 1; + optional sint32 player2_id = 2 [default = -1]; + optional sint32 game_id = 3; + optional sint32 winner_id = 4 [default = -1]; + optional sint32 player1_match_wins = 5 [default = 0]; + optional sint32 player2_match_wins = 6 [default = 0]; +} + +message TournamentSettings { + optional uint32 games_per_match = 1 [default = 1]; +} + +message Event_TournamentState { + extend GameEvent { + optional Event_TournamentState ext = 2027; + } + + enum TournamentPhase { + PHASE_DECK_BUILDING = 0; + PHASE_PLAYING = 1; + PHASE_FINISHED = 2; + } + + optional TournamentPhase phase = 1; + optional uint32 current_round = 2; + optional uint32 total_rounds = 3; + repeated TournamentPlayer players = 4; + repeated TournamentPairing pairings = 5; + optional TournamentSettings settings = 6; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index 2e5b88978..68a79eb6b 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -180,6 +180,15 @@ message GameCommand { /// Server: Server_Player::cmdSetPlaymat /// Client: reflected via player properties changed event SET_PLAYMAT = 1035; + + /// Report the result of a tournament match sub-game. + REPORT_MATCH_RESULT = 1037; + + /// Advance the tournament to the next round. + ADVANCE_TOURNAMENT = 1038; + + /// Select tournament settings. + TOURNAMENT_SETTINGS_SELECT = 1039; } extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto index 7d3147701..b952bfdef 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -34,6 +34,7 @@ message GameEvent { CHANGE_ZONE_PROPERTIES = 2020; REVERSE_TURN = 2021; GAME_LOG_NOTICE = 2022; + TOURNAMENT_STATE = 2027; } optional sint32 player_id = 1 [default = -1]; extensions 100 to max; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index a8c90ec6c..bf4d5310f 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -69,6 +69,12 @@ message Command_CreateGame { // share decklists with all players when selected optional bool share_decklists_on_load = 14; + + // number of games per match in tournament mode (e.g. 3 for best of 3) + optional uint32 games_per_match = 15 [default = 1]; + + // whether this is a tournament game + optional bool is_tournament = 16; } message Command_JoinGame { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto index 9989ae18a..4ac004cf4 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -65,4 +65,7 @@ message ServerInfo_Game { // the current host of the game, which may differ from the creator after a host transfer optional ServerInfo_User host_info = 53; + + // whether this game is a tournament game + optional bool is_tournament = 54; }