feat(command-zone): add logic layer with tests

Implement CommandZoneLogic - the card data management layer for zones.
This layer handles card insertion, removal, and ordering independently
of Qt graphics rendering.

Key components:
- CommandZoneLogic: manages card data within a zone
- AddCardAlgorithm: generic insertion algorithm for ordered zones
- CardZoneLogic modifications: integrate with existing zone hierarchy

Design decisions:
- Separation of data logic from graphics enables unit testing
- addCardImpl uses a stable insertion algorithm for consistent ordering
- Mock card items enable testing without Qt graphics dependencies

Test coverage verifies card insertion behavior across edge cases.
This commit is contained in:
DawnFire42 2026-02-26 15:49:38 -05:00
parent 3c2e4a3fb2
commit 8422357878
10 changed files with 480 additions and 5 deletions

View file

@ -0,0 +1,44 @@
#ifndef ADD_CARD_ALGORITHM_H
#define ADD_CARD_ALGORITHM_H
/**
* @file add_card_algorithm.h
* @brief Shared algorithm for card insertion logic.
*
* This template is used by both production (CommandZoneLogic) and test code
* to ensure consistent behavior and eliminate duplication.
*/
namespace CardZoneAlgorithms
{
/**
* @brief Core logic for adding a card to a zone's card list.
*
* @tparam CardList List type supporting size(), insert(), getContentsKnown()
* @tparam CardType Card type supporting setId(), setCardRef(), resetState(), setVisible()
* @param cards The card list to insert into
* @param card The card to insert
* @param x Target position (negative or out-of-bounds appends to end)
*/
template <typename CardList, typename CardType> void addCardToList(CardList &cards, CardType *card, int x)
{
// Normalize position: negative or beyond range appends to end
if (x < 0 || x >= cards.size()) {
x = static_cast<int>(cards.size());
}
cards.insert(x, card);
// Handle unknown contents
if (!cards.getContentsKnown()) {
card->setId(-1);
card->setCardRef({});
}
card->resetState(true);
card->setVisible(true);
}
} // namespace CardZoneAlgorithms
#endif // ADD_CARD_ALGORITHM_H

View file

@ -38,7 +38,7 @@ CardZoneLogic::CardZoneLogic(Player *_player,
void CardZoneLogic::addCard(CardItem *card, const bool reorganize, const int x, const int y)
{
if (!card) {
qCWarning(CardZoneLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen";
qCWarning(CardZoneLogicLog) << "CardZoneLogic::addCard() card is null; this shouldn't normally happen";
return;
}
@ -92,7 +92,7 @@ CardItem *CardZoneLogic::getCard(int cardId)
{
CardItem *c = cards.findCard(cardId);
if (!c) {
qCWarning(CardZoneLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found";
qCWarning(CardZoneLogicLog) << "CardZoneLogic::getCard: card id=" << cardId << "not found";
return nullptr;
}
// If the card's id is -1, this zone is invisible,
@ -107,7 +107,7 @@ CardItem *CardZoneLogic::getCard(int cardId)
void CardZoneLogic::removeCard(CardItem *card)
{
if (!card) {
qCWarning(CardZoneLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen";
qCWarning(CardZoneLogicLog) << "CardZoneLogic::removeCard: card is null, this shouldn't normally happen";
return;
}
@ -195,6 +195,18 @@ QString CardZoneLogic::getTranslatedName(bool theirOwn, GrammaticalCase gc) cons
return (theirOwn ? tr("their graveyard", "nominative") : tr("%1's graveyard", "nominative").arg(ownerName));
else if (name == ZoneNames::EXILE)
return (theirOwn ? tr("their exile", "nominative") : tr("%1's exile", "nominative").arg(ownerName));
else if (name == ZoneNames::COMMAND)
return (theirOwn ? tr("their command zone", "nominative")
: tr("%1's command zone", "nominative").arg(ownerName));
else if (name == ZoneNames::PARTNER)
return (theirOwn ? tr("their partner zone", "nominative")
: tr("%1's partner zone", "nominative").arg(ownerName));
else if (name == ZoneNames::COMPANION)
return (theirOwn ? tr("their companion zone", "nominative")
: tr("%1's companion zone", "nominative").arg(ownerName));
else if (name == ZoneNames::BACKGROUND)
return (theirOwn ? tr("their background zone", "nominative")
: tr("%1's background zone", "nominative").arg(ownerName));
else if (name == ZoneNames::SIDEBOARD)
switch (gc) {
case CaseLookAtZone:

View file

@ -1,7 +1,7 @@
/**
* @file card_zone_logic.h
* @ingroup GameLogicZones
* @brief TODO: Document this.
* @brief Base class for zone data management, separated from graphics.
*/
#ifndef COCKATRICE_CARD_ZONE_LOGIC_H
@ -22,6 +22,18 @@ class QAction;
class QPainter;
class CardDragItem;
/**
* @class CardZoneLogic
* @brief Abstract base class for zone data management (logic layer).
*
* CardZoneLogic handles card storage, lookup, and manipulation for a zone.
* It is paired with a CardZone graphics object to implement the logic/graphics
* separation pattern used throughout Cockatrice.
*
* Subclasses must implement addCardImpl() to define zone-specific insertion behavior.
*
* @see CardZone
*/
class CardZoneLogic : public QObject
{
Q_OBJECT
@ -111,7 +123,7 @@ protected:
QList<ZoneViewZone *> views;
bool hasCardAttr;
bool isShufflable;
bool alwaysRevealTopCard;
bool alwaysRevealTopCard = false;
virtual void addCardImpl(CardItem *card, int x, int y) = 0;
};

View file

@ -0,0 +1,19 @@
#include "command_zone_logic.h"
#include "../../board/card_item.h"
#include "add_card_algorithm.h"
CommandZoneLogic::CommandZoneLogic(Player *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent)
: CardZoneLogic(_player, _name, _hasCardAttr, _isShufflable, _contentsKnown, parent)
{
}
void CommandZoneLogic::addCardImpl(CardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x);
}

View file

@ -0,0 +1,51 @@
/**
* @file command_zone_logic.h
* @ingroup GameLogicZones
* @brief Logic layer for the command zone, used for Commander format.
*/
#ifndef COCKATRICE_COMMAND_ZONE_LOGIC_H
#define COCKATRICE_COMMAND_ZONE_LOGIC_H
#include "card_zone_logic.h"
/**
* @class CommandZoneLogic
* @brief Logic layer for managing cards in the command zone.
*
* Handles data storage and card management for command zones in Commander format.
* Supports ordered card insertion for drag-and-drop operations.
*
* @see CommandZone for the graphics layer
* @see CardZoneLogic
*/
class CommandZoneLogic : public CardZoneLogic
{
Q_OBJECT
public:
/**
* @brief Constructs a CommandZoneLogic instance.
* @param _player The player who owns this zone
* @param _name Zone name ("command" or "partner")
* @param _hasCardAttr Whether cards in this zone have attributes
* @param _isShufflable Whether the zone can be shuffled
* @param _contentsKnown Whether the zone contents are public knowledge
* @param parent Parent QObject
*/
CommandZoneLogic(Player *_player,
const QString &_name,
bool _hasCardAttr,
bool _isShufflable,
bool _contentsKnown,
QObject *parent = nullptr);
protected:
/**
* @brief Adds a card at position x (y ignored). Appends if x is -1 or out of range.
* @param card Card to add
* @param x Insertion index, or -1 to append
* @param y Unused
*/
void addCardImpl(CardItem *card, int x, int y) override;
};
#endif // COCKATRICE_COMMAND_ZONE_LOGIC_H