feat(command-zone): add commander tax counter system with tests

Implement CommanderTaxCounter - tracks casting cost increases for
commanders that have been cast multiple times.

Key components:
- CommanderTaxCounter: specialized counter with value clamping
- AbstractCounter modifications: support for counter ID system
- CounterGeneral/TranslateCounterName: UI integration for display

Design decisions:
- Counter values clamped to valid range (0 to max)
- Builds on counter_ids.h constants from commit 1
- Uses z_values.h from commit 4 for proper visual layering
- Counter visibility tied to zone state (hidden when zone hidden)

Test coverage:
- commander_tax_counter_test: verifies setValue clamping behavior
- counter_visibility_test: verifies visibility state transitions

Note: Tests include <climits> for INT_MIN usage (required on some compilers
like GCC on Debian 11 where INT_MIN is not transitively included).
This commit is contained in:
DawnFire42 2026-02-26 15:50:30 -05:00
parent 8422357878
commit 7a0a3ff9ac
10 changed files with 694 additions and 8 deletions

View file

@ -31,6 +31,22 @@ target_link_libraries(command_zone_logic_test
add_test(NAME command_zone_logic_test COMMAND command_zone_logic_test)
# ------------------------
# Commander Tax Counter Test
# Tests setValue clamping behavior
# ------------------------
add_executable(commander_tax_counter_test
commander_tax_counter_test.cpp
)
target_link_libraries(commander_tax_counter_test
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME commander_tax_counter_test COMMAND commander_tax_counter_test)
# ------------------------
# Command Zone State Test
# Tests the extracted CommandZoneState pure state machine
@ -53,10 +69,28 @@ target_link_libraries(command_zone_state_test
add_test(NAME command_zone_state_test COMMAND command_zone_state_test)
# ------------------------
# Counter Visibility Test
# Tests counter visibility during zone state transitions
# ------------------------
add_executable(counter_visibility_test
counter_visibility_test.cpp
)
target_link_libraries(counter_visibility_test
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME counter_visibility_test COMMAND counter_visibility_test)
# ------------------------
# Dependencies on gtest
# ------------------------
if(NOT GTEST_FOUND)
add_dependencies(command_zone_logic_test gtest)
add_dependencies(commander_tax_counter_test gtest)
add_dependencies(command_zone_state_test gtest)
add_dependencies(counter_visibility_test gtest)
endif()

View file

@ -0,0 +1,190 @@
/**
* @file commander_tax_counter_test.cpp
* @brief Unit tests for CommanderTaxCounter::setValue
*
* Tests the value clamping behavior for commander tax:
* - Positive values pass through unchanged
* - Zero values pass through unchanged
* - Negative values are clamped to 0
*/
#include <QRectF>
#include <algorithm>
#include <climits>
#include <gtest/gtest.h>
/**
* @brief Testable implementation of CommanderTaxCounter value handling.
*
* Extracts the setValue and boundingRect logic for unit testing without
* requiring Player, ThemeManager, and QGraphicsScene dependencies.
*/
class CommanderTaxCounterTestHarness
{
int value;
int size;
public:
explicit CommanderTaxCounterTestHarness(int _size = 24, int initialValue = 0) : value(initialValue), size(_size)
{
}
/**
* @brief Implementation of CommanderTaxCounter::setValue logic.
*
* Clamps negative values to 0, matching the real implementation:
* AbstractCounter::setValue(qMax(0, _value));
*/
void setValue(int _value)
{
// Clamp to 0 - commander tax cannot be negative
value = std::max(0, _value);
}
[[nodiscard]] int getValue() const
{
return value;
}
/**
* @brief Implementation of CommanderTaxCounter::boundingRect logic.
*
* Returns a square QRectF based on the counter size.
*/
[[nodiscard]] QRectF boundingRect() const
{
return QRectF(0, 0, size, size);
}
[[nodiscard]] int getSize() const
{
return size;
}
};
class CommanderTaxCounterTest : public ::testing::Test
{
protected:
CommanderTaxCounterTestHarness *counter;
void SetUp() override
{
counter = new CommanderTaxCounterTestHarness(24, 0);
}
void TearDown() override
{
delete counter;
}
};
// Test: Positive value is stored correctly
TEST_F(CommanderTaxCounterTest, SetValue_Positive)
{
counter->setValue(5);
EXPECT_EQ(5, counter->getValue()) << "Positive value should be stored unchanged";
}
// Test: Zero value is stored correctly
TEST_F(CommanderTaxCounterTest, SetValue_Zero)
{
counter->setValue(10); // First set to non-zero
counter->setValue(0);
EXPECT_EQ(0, counter->getValue()) << "Zero value should be stored unchanged";
}
// Test: Negative value is clamped to zero
TEST_F(CommanderTaxCounterTest, SetValue_Negative_ClampedToZero)
{
counter->setValue(-5);
EXPECT_EQ(0, counter->getValue()) << "Negative value should be clamped to 0";
}
// Test: Large negative value is clamped to zero
TEST_F(CommanderTaxCounterTest, SetValue_LargeNegative_ClampedToZero)
{
counter->setValue(-1000);
EXPECT_EQ(0, counter->getValue()) << "Large negative value should be clamped to 0";
}
// Test: Boundary condition - negative one
TEST_F(CommanderTaxCounterTest, SetValue_NegativeOne_ClampedToZero)
{
counter->setValue(-1);
EXPECT_EQ(0, counter->getValue()) << "-1 should be clamped to 0";
}
// Test: Large positive value is stored correctly
TEST_F(CommanderTaxCounterTest, SetValue_LargePositive)
{
counter->setValue(100);
EXPECT_EQ(100, counter->getValue()) << "Large positive value should be stored unchanged";
}
// Test: Multiple setValue calls work correctly
TEST_F(CommanderTaxCounterTest, SetValue_MultipleCalls)
{
counter->setValue(5);
EXPECT_EQ(5, counter->getValue());
counter->setValue(10);
EXPECT_EQ(10, counter->getValue());
counter->setValue(-3);
EXPECT_EQ(0, counter->getValue());
counter->setValue(7);
EXPECT_EQ(7, counter->getValue());
}
// Test: BoundingRect returns correct size
TEST_F(CommanderTaxCounterTest, BoundingRect_CorrectSize)
{
QRectF rect = counter->boundingRect();
EXPECT_EQ(0, rect.x()) << "BoundingRect x should be 0";
EXPECT_EQ(0, rect.y()) << "BoundingRect y should be 0";
EXPECT_EQ(24, rect.width()) << "BoundingRect width should equal size";
EXPECT_EQ(24, rect.height()) << "BoundingRect height should equal size";
}
// Test: BoundingRect with different size
TEST_F(CommanderTaxCounterTest, BoundingRect_DifferentSize)
{
delete counter;
counter = new CommanderTaxCounterTestHarness(36, 0);
QRectF rect = counter->boundingRect();
EXPECT_EQ(36, rect.width()) << "BoundingRect width should match constructor size";
EXPECT_EQ(36, rect.height()) << "BoundingRect height should match constructor size";
}
// Test: Initial value from constructor
TEST_F(CommanderTaxCounterTest, Constructor_InitialValue)
{
delete counter;
counter = new CommanderTaxCounterTestHarness(24, 5);
EXPECT_EQ(5, counter->getValue()) << "Initial value from constructor should be set";
}
// Test: INT_MIN is clamped to zero
TEST_F(CommanderTaxCounterTest, SetValue_IntMin_ClampedToZero)
{
counter->setValue(INT_MIN);
EXPECT_EQ(0, counter->getValue()) << "INT_MIN should be clamped to 0";
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,322 @@
/**
* @file counter_visibility_test.cpp
* @brief Unit tests for counter visibility during zone state transitions
*
* Tests the visibility logic for commander tax counters:
* - Commander tax visible when command zone visible
* - Partner tax visible when command zones visible AND partner expanded
* - Counters remain visible during minimize (only height changes)
* - Signal-driven visibility updates
*/
#include <QSignalSpy>
#include <gtest/gtest.h>
/**
* @brief Testable implementation of counter visibility logic.
*
* Extracts the visibility determination logic from
* PlayerGraphicsItem::rearrangeCounters for isolated testing.
*
* @warning SYNC REQUIRED: This harness duplicates visibility logic from
* player_graphics_item.cpp lines 172-195. Update if implementation changes.
*/
class CounterVisibilityTestHarness : public QObject
{
Q_OBJECT
bool commandZonesVisible_;
bool partnerZoneExpanded_;
bool commanderTaxVisible_;
bool partnerTaxVisible_;
public:
explicit CounterVisibilityTestHarness(QObject *parent = nullptr)
: QObject(parent), commandZonesVisible_(true), partnerZoneExpanded_(false), commanderTaxVisible_(false),
partnerTaxVisible_(false)
{
}
/**
* @brief Sets the command zones visibility state.
*
* This simulates the server enabling/disabling command zones
* based on game format (Commander vs other formats).
*/
void setCommandZonesVisible(bool visible)
{
commandZonesVisible_ = visible;
updateCounterVisibility();
}
/**
* @brief Sets the partner zone expanded state.
*
* Simulates user toggling the partner zone via button or menu.
*/
void setPartnerZoneExpanded(bool expanded)
{
partnerZoneExpanded_ = expanded;
updateCounterVisibility();
}
[[nodiscard]] bool isCommanderTaxVisible() const
{
return commanderTaxVisible_;
}
[[nodiscard]] bool isPartnerTaxVisible() const
{
return partnerTaxVisible_;
}
[[nodiscard]] bool areCommandZonesVisible() const
{
return commandZonesVisible_;
}
[[nodiscard]] bool isPartnerZoneExpanded() const
{
return partnerZoneExpanded_;
}
signals:
void commanderTaxVisibilityChanged(bool visible);
void partnerTaxVisibilityChanged(bool visible);
private:
/**
* @brief Implementation of counter visibility logic.
*
* Commander tax: visible when command zones visible
* Partner tax: visible when command zones visible AND partner expanded
*/
void updateCounterVisibility()
{
bool newCommanderVisible = commandZonesVisible_;
bool newPartnerVisible = commandZonesVisible_ && partnerZoneExpanded_;
if (newCommanderVisible != commanderTaxVisible_) {
commanderTaxVisible_ = newCommanderVisible;
emit commanderTaxVisibilityChanged(commanderTaxVisible_);
}
if (newPartnerVisible != partnerTaxVisible_) {
partnerTaxVisible_ = newPartnerVisible;
emit partnerTaxVisibilityChanged(partnerTaxVisible_);
}
}
};
class CounterVisibilityTest : public ::testing::Test
{
protected:
CounterVisibilityTestHarness *harness;
void SetUp() override
{
harness = new CounterVisibilityTestHarness();
}
void TearDown() override
{
delete harness;
}
};
// ============ Commander Tax Visibility Tests ============
// Test: Commander tax visible when zones visible
TEST_F(CounterVisibilityTest, CommanderTax_VisibleWhenZonesVisible)
{
harness->setCommandZonesVisible(true);
EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax should be visible when zones visible";
}
// Test: Commander tax hidden when zones not visible
TEST_F(CounterVisibilityTest, CommanderTax_HiddenWhenZonesNotVisible)
{
harness->setCommandZonesVisible(false);
EXPECT_FALSE(harness->isCommanderTaxVisible()) << "Commander tax should be hidden when zones not visible";
}
// Test: Commander tax visibility independent of partner zone state
TEST_F(CounterVisibilityTest, CommanderTax_IndependentOfPartnerState)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(false);
EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax visible with partner collapsed";
harness->setPartnerZoneExpanded(true);
EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax visible with partner expanded";
}
// ============ Partner Tax Visibility Tests ============
// Test: Partner tax visible when zones visible AND partner expanded
TEST_F(CounterVisibilityTest, PartnerTax_VisibleWhenExpandedAndZonesVisible)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(true);
EXPECT_TRUE(harness->isPartnerTaxVisible()) << "Partner tax should be visible when expanded and zones visible";
}
// Test: Partner tax hidden when partner collapsed
TEST_F(CounterVisibilityTest, PartnerTax_HiddenWhenCollapsed)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(false);
EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should be hidden when partner collapsed";
}
// Test: Partner tax hidden when zones not visible (even if expanded)
TEST_F(CounterVisibilityTest, PartnerTax_HiddenWhenZonesNotVisible)
{
harness->setCommandZonesVisible(false);
harness->setPartnerZoneExpanded(true);
EXPECT_FALSE(harness->isPartnerTaxVisible())
<< "Partner tax should be hidden when zones not visible, even if expanded";
}
// ============ Visibility Transition Tests ============
// Test: Correct sequence during expand
TEST_F(CounterVisibilityTest, ExpandTransition_PartnerBecomesVisible)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(false);
ASSERT_FALSE(harness->isPartnerTaxVisible());
harness->setPartnerZoneExpanded(true);
EXPECT_TRUE(harness->isPartnerTaxVisible()) << "Partner tax should become visible on expand";
}
// Test: Correct sequence during collapse
TEST_F(CounterVisibilityTest, CollapseTransition_PartnerBecomesHidden)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(true);
ASSERT_TRUE(harness->isPartnerTaxVisible());
harness->setPartnerZoneExpanded(false);
EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should become hidden on collapse";
}
// Test: Hiding zones hides both counters
TEST_F(CounterVisibilityTest, HideZones_HidesBothCounters)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(true);
ASSERT_TRUE(harness->isCommanderTaxVisible());
ASSERT_TRUE(harness->isPartnerTaxVisible());
harness->setCommandZonesVisible(false);
EXPECT_FALSE(harness->isCommanderTaxVisible()) << "Commander tax hidden when zones hidden";
EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax hidden when zones hidden";
}
// Test: Showing zones restores commander tax, partner depends on state
TEST_F(CounterVisibilityTest, ShowZones_RestoresCorrectVisibility)
{
harness->setCommandZonesVisible(true);
harness->setPartnerZoneExpanded(false);
harness->setCommandZonesVisible(false);
ASSERT_FALSE(harness->isCommanderTaxVisible());
ASSERT_FALSE(harness->isPartnerTaxVisible());
harness->setCommandZonesVisible(true);
EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax restored when zones shown";
EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax stays hidden (partner still collapsed)";
}
// ============ Signal Emission Tests ============
// Test: Commander tax visibility signal emitted on change
TEST_F(CounterVisibilityTest, CommanderTax_SignalEmittedOnChange)
{
harness->setCommandZonesVisible(true); // Start visible
QSignalSpy spy(harness, &CounterVisibilityTestHarness::commanderTaxVisibilityChanged);
harness->setCommandZonesVisible(false);
ASSERT_EQ(1, spy.count()) << "Signal should be emitted once";
EXPECT_FALSE(spy.at(0).at(0).toBool()) << "Signal argument should be false";
}
// Test: Partner tax visibility signal emitted on change
TEST_F(CounterVisibilityTest, PartnerTax_SignalEmittedOnChange)
{
harness->setCommandZonesVisible(true);
QSignalSpy spy(harness, &CounterVisibilityTestHarness::partnerTaxVisibilityChanged);
harness->setPartnerZoneExpanded(true);
ASSERT_EQ(1, spy.count()) << "Signal should be emitted once";
EXPECT_TRUE(spy.at(0).at(0).toBool()) << "Signal argument should be true";
}
// Test: No signal when visibility unchanged
TEST_F(CounterVisibilityTest, NoSignal_WhenVisibilityUnchanged)
{
harness->setCommandZonesVisible(true);
QSignalSpy spy(harness, &CounterVisibilityTestHarness::commanderTaxVisibilityChanged);
harness->setCommandZonesVisible(true); // Same value
EXPECT_EQ(0, spy.count()) << "Signal should not emit when visibility unchanged";
}
// ============ Edge Cases ============
// Test: Multiple rapid toggles maintain correct state
TEST_F(CounterVisibilityTest, RapidToggles_MaintainCorrectState)
{
harness->setCommandZonesVisible(true);
// Rapid expand/collapse cycle
harness->setPartnerZoneExpanded(true);
EXPECT_TRUE(harness->isPartnerTaxVisible());
harness->setPartnerZoneExpanded(false);
EXPECT_FALSE(harness->isPartnerTaxVisible());
harness->setPartnerZoneExpanded(true);
EXPECT_TRUE(harness->isPartnerTaxVisible());
harness->setPartnerZoneExpanded(false);
EXPECT_FALSE(harness->isPartnerTaxVisible());
// Final state check
EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should be hidden after toggling";
EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax should remain visible throughout";
}
// Test: Initial state has commander hidden (zones not yet set visible)
TEST_F(CounterVisibilityTest, InitialState_BothHidden)
{
// Fresh harness starts with commandZonesVisible_ = true but counters not updated
CounterVisibilityTestHarness freshHarness;
// Before any setters called, counters start hidden
EXPECT_FALSE(freshHarness.isCommanderTaxVisible()) << "Commander tax should start hidden";
EXPECT_FALSE(freshHarness.isPartnerTaxVisible()) << "Partner tax should start hidden";
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
#include "counter_visibility_test.moc"