mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-22 14:53:53 -07:00
Implement in-game navigation with keyboard
Implements a start to the keyboard navigation with the direction arrows and the space key for card selection. Some binds are still needed, but navigating the board is now possible. The feature includes tests for the implemented feature. Closes #5043 Co-authored-by: Manuel Ramos Monge <manuel.monge@tecnico.ulisboa.pt>
This commit is contained in:
parent
3fa377a11c
commit
5acce8998e
22 changed files with 1023 additions and 46 deletions
|
|
@ -75,6 +75,7 @@ target_link_libraries(
|
|||
)
|
||||
|
||||
add_subdirectory(card_zone_algorithms)
|
||||
add_subdirectory(keyboard_navigator_tests)
|
||||
add_subdirectory(carddatabase)
|
||||
add_subdirectory(loading_from_clipboard)
|
||||
add_subdirectory(movecard_tests)
|
||||
|
|
|
|||
32
tests/keyboard_navigator_tests/CMakeLists.txt
Normal file
32
tests/keyboard_navigator_tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
add_executable(keyboard_navigator_test keyboard_card_navigator_test.cpp keyboard_navigator_test_stubs.cpp)
|
||||
|
||||
target_compile_options(keyboard_navigator_test PRIVATE --coverage)
|
||||
target_link_options(keyboard_navigator_test PRIVATE --coverage)
|
||||
|
||||
target_include_directories(
|
||||
keyboard_navigator_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src ${CMAKE_SOURCE_DIR}/cockatrice/src/game
|
||||
${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libcockatrice_interfaces
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
keyboard_navigator_test
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
PRIVATE libcockatrice_settings
|
||||
PRIVATE libcockatrice_interfaces
|
||||
PRIVATE libcockatrice_protocol
|
||||
PRIVATE libcockatrice_card
|
||||
PRIVATE libcockatrice_deck_list
|
||||
PRIVATE libcockatrice_models
|
||||
PRIVATE libcockatrice_rng
|
||||
PRIVATE libcockatrice_network
|
||||
PRIVATE libcockatrice_utility
|
||||
PRIVATE Qt6::Widgets
|
||||
)
|
||||
|
||||
add_test(NAME keyboard_navigator_test COMMAND keyboard_navigator_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(keyboard_navigator_test gtest)
|
||||
endif()
|
||||
229
tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp
Normal file
229
tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#include "game/keyboard_card_navigator.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMetaObject>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// Some tests require us to get the zones out of a player, so instead of changing code we didn't make, we
|
||||
// decided to use this dirty trick.
|
||||
#define private public
|
||||
#include "game/player/player_logic.h"
|
||||
#undef private
|
||||
|
||||
#include "game/board/abstract_card_item.h"
|
||||
#include "game/board/arrow_item.h"
|
||||
#include "game/board/card_item.h"
|
||||
#include "game/board/card_list.h"
|
||||
#include "game/keyboard_card_navigator.cpp"
|
||||
#include "game/zones/card_zone_logic.h"
|
||||
#include "game/zones/hand_zone_logic.h"
|
||||
#include "game/zones/stack_zone_logic.h"
|
||||
#include "game/zones/table_zone_logic.h"
|
||||
#include "keyboard_navigator_test_fakes.h"
|
||||
|
||||
class KeyboardCardNavigatorTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
KeyboardCardNavigator *navigator;
|
||||
FakeHandZoneLogic *handZone;
|
||||
FakeTableZoneLogic *tableZone;
|
||||
FakeStackZoneLogic *stackZone;
|
||||
PlayerLogic *player;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
handZone = new FakeHandZoneLogic("hand");
|
||||
tableZone = new FakeTableZoneLogic("table");
|
||||
stackZone = new FakeStackZoneLogic("stack");
|
||||
// Player is mocked like this, so we fill the zones for the tests.
|
||||
player = (PlayerLogic *)malloc(sizeof(PlayerLogic));
|
||||
new (&player->zones) QMap<QString, CardZoneLogic *>();
|
||||
player->zones.insert("table", tableZone);
|
||||
player->zones.insert("stack", stackZone);
|
||||
player->zones.insert("hand", handZone);
|
||||
navigator = new KeyboardCardNavigator(player);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete navigator;
|
||||
free(player);
|
||||
delete handZone;
|
||||
delete tableZone;
|
||||
delete stackZone;
|
||||
}
|
||||
};
|
||||
|
||||
/* This test verifies the behaviour of spawning the cursor:
|
||||
When pressing the left arrow, it goes to the first card.
|
||||
When pressing the right arrow, it goes to the last card of the zone. */
|
||||
TEST_F(KeyboardCardNavigatorTest, LeftRightArrowTest)
|
||||
{
|
||||
// Set an arbitrary amount of cards for the zone
|
||||
handZone->setDummyCardCount(5);
|
||||
|
||||
// Set the default index
|
||||
navigator->setHoveredCardIndex(-1);
|
||||
|
||||
navigator->setCurrentZone(handZone);
|
||||
|
||||
// Simulate a key press and a card switch
|
||||
QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eRight);
|
||||
|
||||
// Verify the first card is selected
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 0);
|
||||
|
||||
// Reset the index
|
||||
navigator->setHoveredCardIndex(-1);
|
||||
|
||||
// Simulate a key press and a card switch
|
||||
QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eLeft);
|
||||
|
||||
// Check if the last card was selected
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 4);
|
||||
}
|
||||
|
||||
/* This test verifies the moving behaviour of the cursor. */
|
||||
TEST_F(KeyboardCardNavigatorTest, NormalSwitchCards)
|
||||
{
|
||||
// Set an arbitrary amount of cards for the zone
|
||||
handZone->setDummyCardCount(5);
|
||||
navigator->setCurrentZone(handZone);
|
||||
// Select the second card
|
||||
navigator->setHoveredCardIndex(1);
|
||||
|
||||
// If right is pressed, go to index + 1
|
||||
QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eRight);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 2);
|
||||
|
||||
// If left is pressed, go to index - 1
|
||||
QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eLeft);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 1);
|
||||
}
|
||||
|
||||
/* This test verifies the edge case moving behaviour of the cursor.
|
||||
If we are on the first card, and left is pressed, we should go to the
|
||||
last card, and vice-versa */
|
||||
TEST_F(KeyboardCardNavigatorTest, ZoneLoopsTest)
|
||||
{
|
||||
// Set an arbitrary amount of cards for the zone
|
||||
tableZone->setDummyCardCount(2);
|
||||
|
||||
// Select the first card
|
||||
navigator->setCurrentZone(tableZone);
|
||||
navigator->setHoveredCardIndex(0);
|
||||
|
||||
// If we press left, it wraps around to the end, and the zone doesn't change
|
||||
QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eLeft);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 1);
|
||||
EXPECT_EQ(navigator->getCurrentZone(), tableZone);
|
||||
|
||||
// If we press right, it wraps around to the start, and the zone doesn't change
|
||||
QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eRight);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 0);
|
||||
EXPECT_EQ(navigator->getCurrentZone(), tableZone);
|
||||
}
|
||||
|
||||
/* This test verifies the switching of zones if the zone we are currently on is empty. */
|
||||
TEST_F(KeyboardCardNavigatorTest, EmptyZoneLoopTest)
|
||||
{
|
||||
QList<CardZoneLogic *> zonesList;
|
||||
zonesList.append(tableZone);
|
||||
zonesList.append(stackZone);
|
||||
zonesList.append(handZone);
|
||||
|
||||
tableZone->setDummyCardCount(2);
|
||||
stackZone->setDummyCardCount(0); // empty
|
||||
handZone->setDummyCardCount(2);
|
||||
|
||||
// Simulate key up when switching zones
|
||||
CardZoneLogic *newZone = navigator->findZoneWithCards(zonesList, 0, true);
|
||||
// The result should be zone 2
|
||||
EXPECT_EQ(newZone, handZone);
|
||||
|
||||
// Simulate key down when switching zones
|
||||
newZone = navigator->findZoneWithCards(zonesList, 2, false);
|
||||
// The result should be zone 2
|
||||
EXPECT_EQ(newZone, tableZone);
|
||||
}
|
||||
|
||||
/* This test verifies the switching of zones with the up and down keys. */
|
||||
TEST_F(KeyboardCardNavigatorTest, SwitchZoneTest)
|
||||
{
|
||||
tableZone->setDummyCardCount(2);
|
||||
stackZone->setDummyCardCount(2);
|
||||
handZone->setDummyCardCount(2);
|
||||
|
||||
navigator->setCurrentZone(tableZone);
|
||||
navigator->setHoveredCardIndex(0);
|
||||
|
||||
// Simulate key up when switching zones
|
||||
QKeyEvent eUp(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
|
||||
navigator->switchZone(&eUp);
|
||||
// The result should be zone 1 and the card selected is the first one
|
||||
EXPECT_EQ(navigator->getCurrentZone(), stackZone);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 0);
|
||||
|
||||
// Simulate key down when switching zones
|
||||
QKeyEvent eDown(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier);
|
||||
navigator->switchZone(&eDown);
|
||||
// The result should be zone 0 and the card selected is the first one
|
||||
EXPECT_EQ(navigator->getCurrentZone(), tableZone);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 0);
|
||||
}
|
||||
|
||||
/* This test verifies the case where every zone is empty. */
|
||||
TEST_F(KeyboardCardNavigatorTest, EmptyZoneSwitchZones)
|
||||
{
|
||||
QList<CardZoneLogic *> zonesList;
|
||||
zonesList.append(tableZone);
|
||||
zonesList.append(stackZone);
|
||||
zonesList.append(handZone);
|
||||
|
||||
tableZone->setDummyCardCount(0);
|
||||
stackZone->setDummyCardCount(0);
|
||||
handZone->setDummyCardCount(0);
|
||||
|
||||
// The expected zone is the starting one
|
||||
CardZoneLogic *newZone = navigator->findZoneWithCards(zonesList, 0, true);
|
||||
EXPECT_EQ(newZone, tableZone);
|
||||
}
|
||||
|
||||
/* This test verifies the behaviour when someone moves a card with the mouse and the
|
||||
keyboard is used after a zone becomes empty. */
|
||||
TEST_F(KeyboardCardNavigatorTest, ZoneEmptySwitchTest)
|
||||
{
|
||||
tableZone->setDummyCardCount(1);
|
||||
stackZone->setDummyCardCount(0);
|
||||
handZone->setDummyCardCount(1);
|
||||
|
||||
// Set a card as hovered in the table zone
|
||||
navigator->setCurrentZone(tableZone);
|
||||
navigator->setHoveredCardIndex(0);
|
||||
|
||||
// Simulate the user deleting/moving all cards out of the table zone
|
||||
tableZone->setDummyCardCount(0);
|
||||
|
||||
// Now the user presses an arrow key in the empty zone
|
||||
QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier);
|
||||
navigator->switchCardInZone(&eRight);
|
||||
|
||||
// The expected zone is the next one with cards
|
||||
EXPECT_EQ(navigator->getCurrentZone(), handZone);
|
||||
EXPECT_EQ(navigator->getHoveredIndex(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#ifndef KEYBOARD_NAVIGATOR_TEST_FAKES_H
|
||||
#define KEYBOARD_NAVIGATOR_TEST_FAKES_H
|
||||
|
||||
#include "game/board/card_item.h"
|
||||
#include "game/zones/card_zone_logic.h"
|
||||
#include "game/zones/hand_zone_logic.h"
|
||||
#include "game/zones/stack_zone_logic.h"
|
||||
#include "game/zones/table_zone_logic.h"
|
||||
|
||||
// Define safe macro replacements for the visual tests
|
||||
#define setFocus() zValue()
|
||||
#define getVisuallyOrderedHandCards() getCards()
|
||||
|
||||
namespace QApplicationMock
|
||||
{
|
||||
extern bool hasPopup;
|
||||
}
|
||||
|
||||
class FakeCardZoneLogic : public CardZoneLogic
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
FakeCardZoneLogic(const QString &n) : CardZoneLogic(nullptr, n, false, false, true, nullptr), name(n)
|
||||
{
|
||||
}
|
||||
void addCardImpl(CardItem *, int, int) override
|
||||
{
|
||||
}
|
||||
void setDummyCardCount(int count)
|
||||
{
|
||||
cards.clear();
|
||||
for (int i = 0; i < count; i++) {
|
||||
cards.insert(i, (CardItem *)nullptr);
|
||||
}
|
||||
}
|
||||
const QString getName() const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
#define DECLARE_FAKE_ZONE(FakeName, TargetName) \
|
||||
class FakeName : public FakeCardZoneLogic \
|
||||
{ \
|
||||
public: \
|
||||
FakeName(const QString &n) : FakeCardZoneLogic(n) \
|
||||
{ \
|
||||
} \
|
||||
const QMetaObject *metaObject() const override \
|
||||
{ \
|
||||
return &TargetName::staticMetaObject; \
|
||||
} \
|
||||
void *qt_metacast(const char *clname) override \
|
||||
{ \
|
||||
if (!clname) \
|
||||
return nullptr; \
|
||||
if (!strcmp(clname, #TargetName)) \
|
||||
return static_cast<void *>(this); \
|
||||
return FakeCardZoneLogic::qt_metacast(clname); \
|
||||
} \
|
||||
};
|
||||
|
||||
DECLARE_FAKE_ZONE(FakeTableZoneLogic, TableZoneLogic)
|
||||
DECLARE_FAKE_ZONE(FakeStackZoneLogic, StackZoneLogic)
|
||||
DECLARE_FAKE_ZONE(FakeHandZoneLogic, HandZoneLogic)
|
||||
|
||||
#endif // KEYBOARD_NAVIGATOR_TEST_FAKES_H
|
||||
106
tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp
Normal file
106
tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#include "game/board/abstract_card_item.h"
|
||||
#include "game/board/arrow_item.h"
|
||||
#include "game/board/card_item.h"
|
||||
#include "game/board/card_list.h"
|
||||
#include "game/player/player_logic.h"
|
||||
#include "game/zones/card_zone_logic.h"
|
||||
#include "game/zones/hand_zone_logic.h"
|
||||
#include "game/zones/stack_zone_logic.h"
|
||||
#include "game/zones/table_zone_logic.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QMetaObject>
|
||||
#include <QString>
|
||||
|
||||
// Stubs for AbstractCardItem
|
||||
void AbstractCardItem::setHovered(bool)
|
||||
{
|
||||
}
|
||||
|
||||
// Stubs for ArrowItem
|
||||
ArrowItem::ArrowItem(PlayerLogic *, int, ArrowTarget *, ArrowTarget *, QColor const &)
|
||||
: QObject(nullptr), QGraphicsItem(nullptr)
|
||||
{
|
||||
}
|
||||
void ArrowItem::sendCreateArrowCommand(PlayerLogic *, CardItem *, ArrowTarget *, QColor const &, int)
|
||||
{
|
||||
}
|
||||
void ArrowItem::paint(QPainter *, QStyleOptionGraphicsItem const *, QWidget *)
|
||||
{
|
||||
}
|
||||
void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *)
|
||||
{
|
||||
}
|
||||
const QMetaObject *ArrowItem::metaObject() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
void *ArrowItem::qt_metacast(const char *)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
int ArrowItem::qt_metacall(QMetaObject::Call, int, void **)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Stubs for QMetaObject
|
||||
const QMetaObject TableZoneLogic::staticMetaObject = {
|
||||
{&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
const QMetaObject StackZoneLogic::staticMetaObject = {
|
||||
{&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
const QMetaObject HandZoneLogic::staticMetaObject = {
|
||||
{&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
const QMetaObject PlayerLogic::staticMetaObject = {
|
||||
{&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
const QMetaObject CardZoneLogic::staticMetaObject = {
|
||||
{&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}};
|
||||
|
||||
// Stubs for CardList
|
||||
CardList::CardList(bool) : QList<CardItem *>()
|
||||
{
|
||||
}
|
||||
|
||||
// Stubs for CardZoneLogic
|
||||
CardZoneLogic::CardZoneLogic(PlayerLogic *, const QString &, bool, bool, bool _contentsKnown, QObject *)
|
||||
: QObject(nullptr), cards(_contentsKnown)
|
||||
{
|
||||
}
|
||||
CardItem *CardZoneLogic::getCard(int)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
CardItem *CardZoneLogic::takeCard(int, int, bool)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
void CardZoneLogic::addCardImpl(CardItem *, int, int)
|
||||
{
|
||||
}
|
||||
QString CardZoneLogic::getTranslatedName(bool, GrammaticalCase) const
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
const QMetaObject *CardZoneLogic::metaObject() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
void *CardZoneLogic::qt_metacast(const char *)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
int CardZoneLogic::qt_metacall(QMetaObject::Call, int, void **)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// QApplication Mock implementation
|
||||
namespace QApplicationMock
|
||||
{
|
||||
bool hasPopup = false;
|
||||
}
|
||||
QWidget *QApplication::activePopupWidget()
|
||||
{
|
||||
return QApplicationMock::hasPopup ? (QWidget *)1 : nullptr;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue