mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
Merge de03428e51 into 69700438fe
This commit is contained in:
commit
a4802c1ccf
11 changed files with 1107 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -580,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);
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@
|
|||
#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 <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
|
||||
|
|
@ -63,8 +67,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()
|
||||
{
|
||||
|
|
@ -277,6 +282,10 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
|||
event->set_game_started(false);
|
||||
}
|
||||
|
||||
if (tournamentParentGame) {
|
||||
event->set_parent_game_id(tournamentParentGame->getGameId());
|
||||
}
|
||||
|
||||
for (Server_AbstractParticipant *participant : participants.values()) {
|
||||
participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo);
|
||||
}
|
||||
|
|
@ -325,7 +334,14 @@ 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;
|
||||
}
|
||||
|
||||
// Tournament hubs must be host-started and can't lock in a partially filled
|
||||
// bracket: a mere "everyone current is ready" must not start a 1-player or
|
||||
// undersized tournament. startTournament() additionally enforces 2+ players.
|
||||
if (isTournament && !forceStartGame) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -583,6 +599,17 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve
|
|||
bool playerHost = hostId == participant->getPlayerId();
|
||||
participant->prepareDestroy();
|
||||
|
||||
// If this is the tournament hub (not one of its match sub-games), never re-pair
|
||||
// the leaving player: mark them dropped so their matches are awarded and they
|
||||
// disappear from the bracket instead of stalling the tournament.
|
||||
if (tournament && !tournamentParentGame && !spectator) {
|
||||
const int leavingPlayerId = participant->getPlayerId();
|
||||
GameEventStorage tournGes;
|
||||
tournament->dropPlayer(leavingPlayerId);
|
||||
tournament->broadcastTournamentState(tournGes);
|
||||
tournGes.sendToGame(this);
|
||||
}
|
||||
|
||||
if (playerHost) {
|
||||
int newHostId = -1;
|
||||
for (auto *otherPlayer : getPlayers().values()) {
|
||||
|
|
@ -799,6 +826,15 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti
|
|||
}
|
||||
|
||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1));
|
||||
|
||||
// A tournament's bracket/phase/standings live in Event_TournamentState, which
|
||||
// normally only flows on mutation. Without a copy here a late joiner would sit
|
||||
// on an empty bracket until the next round advances, so replay the current
|
||||
// state as part of the join snapshot.
|
||||
if (tournament) {
|
||||
rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER,
|
||||
prepareGameEvent(tournament->buildStateEvent(), -1));
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Game::sendGameEventContainer(GameEventContainer *cont,
|
||||
|
|
@ -877,6 +913,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -927,3 +964,92 @@ 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()));
|
||||
}
|
||||
|
||||
// A tournament with fewer than two players can't produce a valid bracket.
|
||||
if (tournament->getPlayerCount() < 2) {
|
||||
qWarning() << "Cannot start tournament with fewer than 2 players";
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,31 +24,38 @@
|
|||
#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 <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QScopedPointer>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
|
||||
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 +90,14 @@ private:
|
|||
QList<GameReplay *> replayList;
|
||||
GameReplay *currentReplay;
|
||||
|
||||
bool isTournament;
|
||||
TournamentSettings tournamentSettings;
|
||||
Server_Tournament *tournament;
|
||||
QPointer<Server_Game> tournamentParentGame;
|
||||
int tournamentMatchPlayer1Id;
|
||||
int tournamentMatchPlayer2Id;
|
||||
bool disconnectRemovesPlayer;
|
||||
|
||||
QScopedPointer<Server_DeckValidationStrategy> deckValidationStrategy;
|
||||
|
||||
QScopedPointer<Server_GameLifecycleStrategy> lifecycleStrategy;
|
||||
|
|
@ -222,6 +237,49 @@ 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);
|
||||
QPointer<Server_Game> getTournamentParentGame() const
|
||||
{
|
||||
return tournamentParentGame;
|
||||
}
|
||||
bool getDisconnectRemovesPlayer() const
|
||||
{
|
||||
return disconnectRemovesPlayer;
|
||||
}
|
||||
void setDisconnectRemovesPlayer(bool _disconnectRemovesPlayer)
|
||||
{
|
||||
disconnectRemovesPlayer = _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();
|
||||
|
|
@ -234,6 +292,8 @@ public:
|
|||
{
|
||||
return lifecycleStrategy.data();
|
||||
}
|
||||
/** @brief Replace the match result strategy; takes ownership of @p strategy. */
|
||||
void setMatchResultStrategy(Server_MatchResultStrategy *strategy);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,691 @@
|
|||
#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 <QLoggingCategory>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
|
||||
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)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
TournamentPlayerData data;
|
||||
data.playerId = playerId;
|
||||
data.playerName = playerName;
|
||||
data.dropped = false;
|
||||
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);
|
||||
submittedDecks.remove(playerId);
|
||||
byeGivenPlayers.remove(playerId);
|
||||
}
|
||||
|
||||
void Server_Tournament::dropPlayer(int playerId)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
if (!players.contains(playerId)) {
|
||||
return;
|
||||
}
|
||||
players[playerId].dropped = true;
|
||||
players[playerId].deckSubmitted = false;
|
||||
|
||||
// Any current pairing that involves the dropped player and is not already
|
||||
// decided is awarded to the surviving opponent (or recorded as undecided if
|
||||
// both dropped). The opponent keeps playing without sitting out a round.
|
||||
for (auto &pairing : currentPairings) {
|
||||
if (pairing.winnerId != -2) {
|
||||
continue;
|
||||
}
|
||||
bool involvesDropped = (pairing.player1Id == playerId || pairing.player2Id == playerId);
|
||||
if (!involvesDropped) {
|
||||
continue;
|
||||
}
|
||||
if (pairing.player1Id == playerId && pairing.player2Id == playerId) {
|
||||
continue;
|
||||
}
|
||||
int opponent = (pairing.player1Id == playerId) ? pairing.player2Id : pairing.player1Id;
|
||||
if (players.contains(opponent) && !players[opponent].dropped) {
|
||||
pairing.winnerId = opponent;
|
||||
players[opponent].wins += 1;
|
||||
players[playerId].losses += 1;
|
||||
}
|
||||
allPreviousPairings.append(qMakePair(pairing.player1Id, pairing.player2Id));
|
||||
}
|
||||
}
|
||||
|
||||
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<int> available;
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
if (!it->dropped) {
|
||||
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;
|
||||
});
|
||||
|
||||
QSet<int> paired;
|
||||
// Try to pair every player, allowing a single rematch only if the greedy pass
|
||||
// would otherwise leave any unpaired remainder. Dropped players are never paired.
|
||||
int maxRematches = available.size() / 2;
|
||||
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;
|
||||
}
|
||||
bool rematch = havePlayed(available[i], available[j]);
|
||||
if (rematch && maxRematches <= 0) {
|
||||
continue;
|
||||
}
|
||||
TournamentPairingData pairing;
|
||||
pairing.player1Id = available[i];
|
||||
pairing.player2Id = available[j];
|
||||
currentPairings.append(pairing);
|
||||
paired.insert(available[i]);
|
||||
paired.insert(available[j]);
|
||||
if (rematch) {
|
||||
--maxRematches;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Give a bye to every remaining unpaired eligible player, worst-ranked first.
|
||||
// A player receives at most one bye over the whole tournament.
|
||||
QList<int> unpaired;
|
||||
for (int id : available) {
|
||||
if (!paired.contains(id)) {
|
||||
unpaired.append(id);
|
||||
}
|
||||
}
|
||||
// Byes go to the lowest-ranked eligible player who has not had one yet.
|
||||
std::sort(unpaired.begin(), unpaired.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;
|
||||
});
|
||||
|
||||
for (int id : unpaired) {
|
||||
if (byeGivenPlayers.contains(id)) {
|
||||
// Already used a bye: a dropped opponent or earlier bye means this player
|
||||
// simply sits out the round with a free win to keep the bracket moving.
|
||||
TournamentPairingData bye;
|
||||
bye.player1Id = id;
|
||||
bye.player2Id = -1;
|
||||
bye.winnerId = id;
|
||||
currentPairings.append(bye);
|
||||
continue;
|
||||
}
|
||||
TournamentPairingData bye;
|
||||
bye.player1Id = id;
|
||||
bye.player2Id = -1;
|
||||
bye.winnerId = id;
|
||||
currentPairings.append(bye);
|
||||
players[id].wins += 1;
|
||||
byeGivenPlayers.insert(id);
|
||||
allPreviousPairings.append(qMakePair(id, -1));
|
||||
}
|
||||
}
|
||||
|
||||
int Server_Tournament::calculateTotalRounds() const
|
||||
{
|
||||
int n = 0;
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
if (!it->dropped) {
|
||||
++n;
|
||||
}
|
||||
}
|
||||
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<QPair<int, int>> planned;
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.player2Id == -1 || pairing.winnerId != -2) {
|
||||
continue;
|
||||
}
|
||||
if (players.value(pairing.player1Id).dropped || players.value(pairing.player2Id).dropped) {
|
||||
continue;
|
||||
}
|
||||
if (pairing.matchGameIds.size() >= static_cast<int>(gamesPerMatch)) {
|
||||
continue;
|
||||
}
|
||||
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<int>(gamesPerMatch)) {
|
||||
qCWarning(TournamentLog) << "Refusing to exceed series length for pairing" << player1Id << player2Id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bail out if either participant is no longer connected: a match game with
|
||||
// zero or one connected player can never finish and would stall the round.
|
||||
if (!matchGameFactory->getUserInterface(player1Name) || !matchGameFactory->getUserInterface(player2Name)) {
|
||||
qCWarning(TournamentLog) << "Skipping match creation: a player in pairing" << player1Id << player2Id
|
||||
<< "is no longer connected";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a sub-game for this match via the factory, copying the real
|
||||
// ServerInfo_User so it ships the true user level rather than a fabricated
|
||||
// admin identity that would surface in buddy/ignore-list checks.
|
||||
ServerInfo_User creatorInfo;
|
||||
if (auto *ui = matchGameFactory->getUserInterface(player1Name)) {
|
||||
creatorInfo = *ui->getUserInfo();
|
||||
} else {
|
||||
creatorInfo.set_name(player1Name.toStdString());
|
||||
}
|
||||
|
||||
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);
|
||||
// A disconnect inside a tournament match must remove the player so the match
|
||||
// can be decided; it must not leave them sitting as a half-present participant.
|
||||
matchGame->setDisconnectRemovesPlayer(true);
|
||||
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.
|
||||
// Both UI lookups were verified above, so a player can only drop between that
|
||||
// check and this add — in which case they get handled by drop processing and
|
||||
// the pairing settles on the surviving opponent.
|
||||
QMap<int, QPair<Server_AbstractUserInterface *, ResponseContainer *>> 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.
|
||||
bool anyDeckMissing = false;
|
||||
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);
|
||||
} else {
|
||||
anyDeckMissing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyDeckMissing) {
|
||||
// Not every participant submitted a deck. Do not force-start: that would
|
||||
// kick the players without a deck. Leave the match game open so they can
|
||||
// select a deck; the host starts it through the normal ready flow.
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the match game without forcing: both participants are ready and have
|
||||
// decks, so there is nothing to kick.
|
||||
matchGame->startGameIfReady(false);
|
||||
}
|
||||
|
||||
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): counts nothing toward the series but does consume
|
||||
// a slot, so a series can still end in a draw when it is exhausted.
|
||||
|
||||
// The winner needs a strict majority of the games in the series.
|
||||
const int gamesPlayed = pairingPtr->matchGameIds.size();
|
||||
const int gamesNeeded = static_cast<int>(gamesPerMatch / 2 + 1);
|
||||
const int gamesRemaining = static_cast<int>(gamesPerMatch) - gamesPlayed;
|
||||
matchDecided = (pairingPtr->player1MatchWins >= gamesNeeded) || (pairingPtr->player2MatchWins >= gamesNeeded);
|
||||
if (!matchDecided) {
|
||||
// Series exhausted without a strict-majority winner (e.g. a drawn Bo3
|
||||
// leaves it 1-1): record the match as a draw so the round always advances.
|
||||
matchDecided = (gamesRemaining <= 0) && (pairingPtr->player1MatchWins == pairingPtr->player2MatchWins);
|
||||
}
|
||||
|
||||
if (matchDecided) {
|
||||
// Determine match winner
|
||||
int matchWinnerId = -1;
|
||||
if (pairingPtr->player1MatchWins >= gamesNeeded) {
|
||||
matchWinnerId = pairingPtr->player1Id;
|
||||
} else if (pairingPtr->player2MatchWins >= gamesNeeded) {
|
||||
matchWinnerId = pairingPtr->player2Id;
|
||||
}
|
||||
// Otherwise the series was exhausted evenly — matchWinnerId stays -1 (a draw).
|
||||
|
||||
// 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 if (matchWinnerId == pairingPtr->player2Id) {
|
||||
players[pairingPtr->player2Id].wins += 1;
|
||||
players[pairingPtr->player1Id].losses += 1;
|
||||
} else {
|
||||
players[pairingPtr->player1Id].draws += 1;
|
||||
players[pairingPtr->player2Id].draws += 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);
|
||||
}
|
||||
}
|
||||
|
||||
Event_TournamentState Server_Tournament::buildStateEvent() const
|
||||
{
|
||||
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);
|
||||
// -2 = undecided; a decided draw is -1. The is_draw bit distinguishes a
|
||||
// reported draw from an unset winner_id on the wire.
|
||||
if (pairing.winnerId == -1) {
|
||||
p->set_is_draw(true);
|
||||
} else if (pairing.winnerId != -2) {
|
||||
p->set_winner_id(pairing.winnerId);
|
||||
}
|
||||
p->set_player1_match_wins(pairing.player1MatchWins);
|
||||
p->set_player2_match_wins(pairing.player2MatchWins);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void Server_Tournament::broadcastTournamentState(GameEventStorage &ges)
|
||||
{
|
||||
ges.enqueueGameEvent(buildStateEvent(), -1);
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
#ifndef SERVER_TOURNAMENT_H
|
||||
#define SERVER_TOURNAMENT_H
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QRecursiveMutex>
|
||||
#include <QSet>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
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);
|
||||
// Marks an already-starting/started tournament player as dropped: they stop
|
||||
// being paired and their outstanding unstarted match is awarded as a loss.
|
||||
void dropPlayer(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);
|
||||
// Current tournament state message, for replaying to a participant joining late.
|
||||
Event_TournamentState buildStateEvent() const;
|
||||
|
||||
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;
|
||||
bool dropped = 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<int> matchGameIds;
|
||||
};
|
||||
|
||||
private:
|
||||
QPointer<Server_Game> parentGame;
|
||||
Server_MatchGameFactory *matchGameFactory;
|
||||
mutable QRecursiveMutex tournamentMutex;
|
||||
QMap<int, TournamentPlayerData> players;
|
||||
QMap<int, DeckList *> submittedDecks;
|
||||
QList<TournamentPairingData> currentPairings;
|
||||
// Players that have already received a bye in a previous round, so no one
|
||||
// gets more than one bye over the whole tournament.
|
||||
QSet<int> byeGivenPlayers;
|
||||
QList<QPair<int, int>> 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
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#include "server_tournament_lifecycle_strategy.h"
|
||||
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_game.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
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().data() != 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
#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 <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
bool Server_TournamentMatchResultStrategy::onGameFinished(Server_Game *game,
|
||||
int playing,
|
||||
Server_AbstractPlayer *lastPlayer)
|
||||
{
|
||||
// The hub game is owned by the room and may be torn down once its host leaves
|
||||
// and no players remain, while the match sub-games keep running. QPointer keeps
|
||||
// this link checked so a later-finishing match can't touch freed memory.
|
||||
auto *parentGame = game->getTournamentParentGame().data();
|
||||
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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -946,6 +947,9 @@ 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_tournament_settings() ? static_cast<int>(cmd.tournament_settings().games_per_match()) : 1;
|
||||
|
||||
const int gameId = databaseInterface->getNextGameId();
|
||||
if (gameId == -1) {
|
||||
|
|
@ -970,6 +974,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<uint32_t>(qBound(1, gamesPerMatch, MAX_GAMES_PER_MATCH)));
|
||||
}
|
||||
|
||||
game->addPlayer(this, rc, asSpectator, asJudge, false);
|
||||
room->addGame(game);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue