PR 0: introduce GameConfig, strategy pattern, and refactor Server_Game constructor

- Add GameConfig struct consolidating 14 construction parameters
- Add strategy interfaces for deck validation, game lifecycle, and match results
- Add Server_MatchGameFactory abstract factory (forward-looking for tournaments)
- Replace 15-arg Server_Game constructor with (GameConfig, Server_Room*)
- Wire default strategies into Server_Game with lifecycle/match hooks
- Update server_protocolhandler to build GameConfig from protobuf
- Update reverse_card_move_test to use new constructor
- Update CMakeLists.txt with new headers

Took 7 minutes

Took 2 minutes
This commit is contained in:
Lukas Brübach 2026-07-29 09:16:03 +02:00
parent be6a783c12
commit 1c62588c38
10 changed files with 219 additions and 44 deletions

View file

@ -10,7 +10,11 @@ set(HEADERS
game/server_card.h
game/server_cardzone.h
game/server_counter.h
game/game_config.h
game/server_deck_validation_strategy.h
game/server_game.h
game/server_game_lifecycle_strategy.h
game/server_match_result_strategy.h
game/server_player.h
game/server_spectator.h
server.h

View file

@ -0,0 +1,27 @@
#ifndef GAME_CONFIG_H
#define GAME_CONFIG_H
#include <QList>
#include <QString>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
class GameConfig
{
public:
ServerInfo_User creatorInfo;
int gameId = -1;
QString description;
QString password;
int maxPlayers = 2;
QList<int> gameTypes;
bool onlyBuddies = false;
bool onlyRegistered = false;
bool spectatorsAllowed = false;
bool spectatorsNeedPassword = false;
bool spectatorsCanTalk = true;
bool spectatorsSeeEverything = true;
int startingLifeTotal = 20;
bool shareDecklistsOnLoad = true;
};
#endif

View file

@ -0,0 +1,29 @@
#ifndef SERVER_DECK_VALIDATION_STRATEGY_H
#define SERVER_DECK_VALIDATION_STRATEGY_H
#include <libcockatrice/protocol/pb/response.pb.h>
class DeckList;
class Server_Game;
class Server_Player;
class ResponseContainer;
class Server_DeckValidationStrategy
{
public:
virtual ~Server_DeckValidationStrategy() = default;
virtual Response::ResponseCode
validate(Server_Game *game, Server_Player *player, DeckList *deck, ResponseContainer &rc) = 0;
};
class Server_DefaultDeckValidationStrategy : public Server_DeckValidationStrategy
{
public:
Response::ResponseCode validate(Server_Game *, Server_Player *, DeckList *, ResponseContainer &) override
{
return Response::RespOk;
}
};
#endif

View file

@ -53,34 +53,22 @@
#include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/utility/zone_names.h>
Server_Game::Server_Game(const ServerInfo_User &_creatorInfo,
int _gameId,
const QString &_description,
const QString &_password,
int _maxPlayers,
const QList<int> &_gameTypes,
bool _onlyBuddies,
bool _onlyRegistered,
bool _spectatorsAllowed,
bool _spectatorsNeedPassword,
bool _spectatorsCanTalk,
bool _spectatorsSeeEverything,
int _startingLifeTotal,
bool _shareDecklistsOnLoad,
Server_Room *_room)
: QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)),
gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers),
gameTypes(_gameTypes), activePlayer(-1), activePhase(-1), onlyBuddies(_onlyBuddies),
onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed),
spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk),
spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal),
shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0),
firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
Server_Game::Server_Game(const GameConfig &config, Server_Room *_room)
: QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(config.creatorInfo)),
gameStarted(false), gameClosed(false), gameId(config.gameId), description(config.description.simplified()),
password(config.password), maxPlayers(config.maxPlayers), gameTypes(config.gameTypes), activePlayer(-1),
activePhase(-1), onlyBuddies(config.onlyBuddies), onlyRegistered(config.onlyRegistered),
spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword),
spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything),
startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad),
inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false),
turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr),
deckValidationStrategy(new Server_DefaultDeckValidationStrategy),
matchResultStrategy(new Server_NullMatchResultStrategy), lifecycleStrategy(new Server_DefaultLifecycleStrategy),
gameMutex()
{
currentReplay = new GameReplay;
currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId());
description = _description.simplified();
connect(this, &Server_Game::sigStartGameIfReady, this, &Server_Game::doStartGameIfReady, Qt::QueuedConnection);
@ -118,6 +106,10 @@ Server_Game::~Server_Game()
for (auto *replay : replayList) {
delete replay;
}
delete deckValidationStrategy;
delete matchResultStrategy;
delete lifecycleStrategy;
replayList.clear();
room = nullptr;
@ -343,6 +335,13 @@ void Server_Game::doStartGameIfReady(bool forceStartGame)
}
players = getPlayers(); // players could have been kicked, get new list of players
// Delegate pre-start logic (draft/tournament) to lifecycle strategy
if (lifecycleStrategy->onGameStarting(this) == Server_GameLifecycleStrategy::StartAction::Handled) {
locker.unlock();
return;
}
for (Server_AbstractPlayer *player : players.values()) {
player->setupZones();
}
@ -400,10 +399,12 @@ void Server_Game::stopGameIfFinished()
QMutexLocker locker(&gameMutex);
int playing = 0;
Server_AbstractPlayer *lastPlayer = nullptr;
auto players = getPlayers();
for (auto *player : players.values()) {
if (!player->getConceded()) {
++playing;
lastPlayer = player;
}
}
if (playing > 1) {
@ -419,6 +420,17 @@ void Server_Game::stopGameIfFinished()
sendGameStateToPlayers();
// Delegate post-game actions (e.g., tournament reporting) to the match result strategy
bool matchDecided = matchResultStrategy->onGameFinished(this, playing, lastPlayer);
if (matchDecided) {
locker.unlock();
sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1));
gameClosed = true;
deleteLater();
return;
}
locker.unlock();
ServerInfo_Game gameInfo;
@ -901,3 +913,11 @@ void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPl
}
}
}
void Server_Game::setDeckValidationStrategy(Server_DeckValidationStrategy *strategy)
{
if (deckValidationStrategy) {
delete deckValidationStrategy;
}
deckValidationStrategy = strategy;
}

View file

@ -21,6 +21,10 @@
#define SERVERGAME_H
#include "../server_response_containers.h"
#include "game_config.h"
#include "server_deck_validation_strategy.h"
#include "server_game_lifecycle_strategy.h"
#include "server_match_result_strategy.h"
#include <QDateTime>
#include <QMap>
@ -78,6 +82,10 @@ private:
QList<GameReplay *> replayList;
GameReplay *currentReplay;
Server_DeckValidationStrategy *deckValidationStrategy;
Server_MatchResultStrategy *matchResultStrategy;
Server_GameLifecycleStrategy *lifecycleStrategy;
void createGameStateChangedEvent(Event_GameStateChanged *event,
Server_AbstractParticipant *recipient,
bool omniscient,
@ -92,21 +100,7 @@ private slots:
public:
mutable QRecursiveMutex gameMutex;
Server_Game(const ServerInfo_User &_creatorInfo,
int _gameId,
const QString &_description,
const QString &_password,
int _maxPlayers,
const QList<int> &_gameTypes,
bool _onlyBuddies,
bool _onlyRegistered,
bool _spectatorsAllowed,
bool _spectatorsNeedPassword,
bool _spectatorsCanTalk,
bool _spectatorsSeeEverything,
int _startingLifeTotal,
bool _shareDecklistsOnLoad,
Server_Room *parent);
Server_Game(const GameConfig &config, Server_Room *parent);
~Server_Game() override;
Server_Room *getRoom() const
{
@ -221,6 +215,16 @@ public:
GameEventStorageItem::SendToOthers,
int privatePlayerId = -1);
void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player);
Server_DeckValidationStrategy *getDeckValidationStrategy() const
{
return deckValidationStrategy;
}
Server_GameLifecycleStrategy *getLifecycleStrategy() const
{
return lifecycleStrategy;
}
void setDeckValidationStrategy(Server_DeckValidationStrategy *strategy);
};
#endif

View file

@ -0,0 +1,29 @@
#ifndef SERVER_GAME_LIFECYCLE_STRATEGY_H
#define SERVER_GAME_LIFECYCLE_STRATEGY_H
class Server_Game;
class Server_GameLifecycleStrategy
{
public:
virtual ~Server_GameLifecycleStrategy() = default;
enum class StartAction
{
ProceedNormal,
Handled,
};
virtual StartAction onGameStarting(Server_Game *game) = 0;
};
class Server_DefaultLifecycleStrategy : public Server_GameLifecycleStrategy
{
public:
StartAction onGameStarting(Server_Game *) override
{
return StartAction::ProceedNormal;
}
};
#endif

View file

@ -0,0 +1,18 @@
#ifndef SERVER_MATCH_GAME_FACTORY_H
#define SERVER_MATCH_GAME_FACTORY_H
class GameConfig;
class Server_Game;
class Server_AbstractUserInterface;
class Server_MatchGameFactory
{
public:
virtual ~Server_MatchGameFactory() = default;
virtual Server_Game *createMatchGame(const GameConfig &config, int &outGameId) = 0;
virtual Server_AbstractUserInterface *getUserInterface(const QString &playerName) = 0;
virtual void addGameToRoom(Server_Game *game) = 0;
};
#endif

View file

@ -0,0 +1,24 @@
#ifndef SERVER_MATCH_RESULT_STRATEGY_H
#define SERVER_MATCH_RESULT_STRATEGY_H
class Server_AbstractPlayer;
class Server_Game;
class Server_MatchResultStrategy
{
public:
virtual ~Server_MatchResultStrategy() = default;
virtual bool onGameFinished(Server_Game *game, int playing, Server_AbstractPlayer *lastPlayer) = 0;
};
class Server_NullMatchResultStrategy : public Server_MatchResultStrategy
{
public:
bool onGameFinished(Server_Game *, int, Server_AbstractPlayer *) override
{
return false;
}
};
#endif

View file

@ -1,5 +1,6 @@
#include "server_protocolhandler.h"
#include "game/game_config.h"
#include "game/server_game.h"
#include "game/server_player.h"
#include "server_database_interface.h"
@ -885,10 +886,23 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
// When server doesn't permit registered users to exist, do not honor only-reg setting
bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers());
auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()),
cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers,
cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(),
cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room);
GameConfig config;
config.creatorInfo = copyUserInfo(false);
config.gameId = gameId;
config.description = description;
config.password = QString::fromStdString(cmd.password());
config.maxPlayers = cmd.max_players();
config.gameTypes = gameTypes;
config.onlyBuddies = cmd.only_buddies();
config.onlyRegistered = onlyRegisteredUsers;
config.spectatorsAllowed = cmd.spectators_allowed();
config.spectatorsNeedPassword = cmd.spectators_need_password();
config.spectatorsCanTalk = cmd.spectators_can_talk();
config.spectatorsSeeEverything = cmd.spectators_see_everything();
config.startingLifeTotal = startingLifeTotal;
config.shareDecklistsOnLoad = shareDecklistsOnLoad;
auto *game = new Server_Game(config, room);
game->addPlayer(this, rc, asSpectator, asJudge, false);
room->addGame(game);

View file

@ -1,3 +1,4 @@
#include "game/game_config.h"
#include "game/server_abstract_player.h"
#include "game/server_card.h"
#include "game/server_cardzone.h"
@ -22,7 +23,12 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest)
// instantiate a fake server instance
FakeServer server;
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
Server_Game game(user, 1, "", "", 2, QList<int>(), false, false, false, false, false, false, 20, false, &room);
GameConfig config;
config.creatorInfo = user;
config.gameId = 1;
config.maxPlayers = 2;
config.startingLifeTotal = 20;
Server_Game game(config, &room);
Server_AbstractPlayer player(&game, 1, user, false, nullptr);
Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone);
Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone);