Files

426 lines
14 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "ScriptTest.h"
#include "card/Card.h"
#include "common/ECS.h"
#include <cmath>
using namespace eve::card;
namespace {
int countCardView() {
int n = 0;
auto view = ecs::View<CardData, CardData::Identity>();
for (auto it = view.begin(); it != view.end(); ++it) ++n;
return n;
}
bool near(float a, float b, float eps = 1e-3f) { return std::fabs(a - b) < eps; }
const char *kDefs = R"JSON(
[{"id":"flame","name":"Flame","kind":"creature","cost":2,"attack":3,"health":2},
{"id":"bolt","name":"Bolt","kind":"spell","cost":3}]
)JSON";
} // namespace
TEST_CASE("card.factory.newCardFromJson") {
Card mod;
CHECK_EQ(mod.registerCardsFromJson(kDefs), 2);
CHECK(mod.hasCardDefinition("flame"));
CardData *c = mod.newCard("flame");
REQUIRE(c != nullptr);
CHECK_EQ(c->identity()->name, std::string("Flame"));
CHECK_EQ(c->stats()->cost, 2);
CHECK_EQ(c->stats()->attack, 3);
CHECK_EQ(mod.newCard("missing"), nullptr);
}
TEST_CASE("card.authoring.stableDefinitionEnumerationAndMutation") {
Card mod;
REQUIRE_EQ(mod.registerCardsFromJson(kDefs), 2);
CHECK_EQ(mod.getCardDefinitionId(0), std::string("bolt"));
CHECK_EQ(mod.getCardDefinitionId(1), std::string("flame"));
REQUIRE(mod.setCardDefinition("archer", "Archer", "creature", 4, 5, 3, 1.2f, -0.2f, 0.5f));
CHECK_EQ(mod.getCardDefinitionId(0), std::string("archer"));
CHECK_EQ(mod.getCardDefinitionAttack("archer"), 5);
CHECK_EQ(mod.getCardDefinitionTintR("archer"), 1.f);
CHECK_EQ(mod.getCardDefinitionTintG("archer"), 0.f);
CHECK(!mod.setCardDefinition("", "Invalid", "", 0, 0, 0, 0.f, 0.f, 0.f));
REQUIRE(mod.removeCardDefinition("bolt"));
CHECK(!mod.hasCardDefinition("bolt"));
CHECK(!mod.removeCardDefinition("bolt"));
}
TEST_CASE("card.presentation.stableIdentityAndSnapshot") {
Card mod;
CHECK_EQ(mod.registerCardsFromJson(kDefs), 2);
CardData *first = mod.newCard("flame");
CardData *second = mod.newCard("flame");
REQUIRE(first != nullptr);
REQUIRE(second != nullptr);
CHECK_EQ(first->identity()->definitionId, "flame");
CHECK_EQ(second->identity()->definitionId, "flame");
CHECK_NE(first->identity()->id, second->identity()->id);
first->layout()->x = 42.f;
first->layout()->angle = 12.f;
CHECK_EQ(mod.capturePresentation(), 2);
auto *snapshot = mod.getPresentation(0);
REQUIRE(snapshot != nullptr);
CHECK_EQ(snapshot->instanceId, first->identity()->id);
CHECK_EQ(snapshot->definitionId, "flame");
CHECK_EQ(snapshot->x, 42.f);
CHECK_EQ(snapshot->angle, 12.f);
CHECK_EQ(mod.getPresentation(2), nullptr);
}
TEST_CASE("card.presentation.planeMapperRoundTrip") {
CardPlaneMapper mapper;
mapper.setLogicalRect(0.f, 0.f, 1280.f, 700.f);
mapper.setPlane(-6.4f, 0.6f, -7.f, 12.8f, 0.f, 0.f, 0.f, 0.f, 14.f);
CHECK(mapper.mapLayout(640.f, 350.f));
CHECK_LT(std::abs(mapper.getWorldX()), 0.0001f);
CHECK_LT(std::abs(mapper.getWorldY() - 0.6f), 0.0001f);
CHECK_LT(std::abs(mapper.getWorldZ()), 0.0001f);
CHECK(mapper.mapRay(0.f, 10.f, 0.f, 0.f, -1.f, 0.f));
CHECK_LT(std::abs(mapper.getLogicalX() - 640.f), 0.0001f);
CHECK_LT(std::abs(mapper.getLogicalY() - 350.f), 0.0001f);
CHECK(!mapper.mapRay(0.f, 10.f, 0.f, 1.f, 0.f, 0.f));
}
TEST_CASE("card.hand.membershipOrder") {
Card mod;
mod.registerCardsFromJson(kDefs);
LayoutConfig *cfg = mod.newConfig();
Hand *h = mod.newHand(cfg);
h->meta()->owner = "player";
CardData *a = mod.newCard("flame");
CardData *b = mod.newCard("bolt");
h->addCard(a);
h->addCard(b);
CHECK_EQ(h->count(), 2);
CHECK_EQ(h->get(0), a);
CHECK_EQ(h->get(1), b);
CHECK_EQ(mod.findHand("player"), h);
}
TEST_CASE("card.deck.stackOrder") {
Card mod;
mod.registerCardsFromJson(kDefs);
Deck *d = mod.newDeck();
CardData *a = mod.newCard("flame");
CardData *b = mod.newCard("bolt");
d->push(a);
d->push(b);
CHECK_EQ(d->count(), 2);
CHECK_EQ(d->peek(), b);
CHECK_EQ(d->draw(), b);
CHECK_EQ(d->draw(), a);
CHECK(d->isEmpty());
}
TEST_CASE("card.ecs.viewCountsFactoryCards") {
const int before = countCardView();
{
Card mod;
mod.registerCardsFromJson(kDefs);
CHECK(mod.newCard("flame") != nullptr);
CHECK(mod.newCard("bolt") != nullptr);
CHECK_EQ(countCardView(), before + 2);
}
CHECK_EQ(countCardView(), before);
}
TEST_CASE("card.ecs.removeKeepsEntityInView") {
Card mod;
mod.registerCardsFromJson(kDefs);
const int before = countCardView();
LayoutConfig *cfg = mod.newConfig();
Hand *h = mod.newHand(cfg);
CardData *c = mod.newCard("flame");
h->addCard(c);
CHECK_EQ(countCardView(), before + 1);
CHECK(h->removeCard(c));
CHECK_EQ(h->count(), 0);
CHECK_EQ(countCardView(), before + 1);
CHECK_EQ(c->identity()->name, std::string("Flame"));
}
TEST_CASE("card.drawCardMovesDeckToHand") {
Card mod;
mod.registerCardsFromJson(kDefs);
LayoutConfig *cfg = mod.newConfig();
Hand *h = mod.newHand(cfg);
h->meta()->owner = "player";
Deck *d = mod.newDeck();
d->push(mod.newCard("flame"));
CardData *drawn = mod.drawCard("player");
REQUIRE(drawn != nullptr);
CHECK_EQ(h->count(), 1);
CHECK(d->isEmpty());
CHECK_EQ(h->get(0), drawn);
}
static const char *kCardViewScript = R"SQ(
function testCardCppView() {
local card = eve.Card()
card.registerCardsFromJson("[{\"id\":\"flame\",\"name\":\"Flame\"}]")
local c = card.newCard("flame")
if (c == null) return false
local all = eve.view(eve.CardData)
foreach (e in all) {
if (e.getId() == c.getId()) return true
}
return false
}
)SQ";
UnitSciptTest(CardViewScriptTest, kCardViewScript);
TEST_CASE_FIXTURE(CardViewScriptTest, "card.script.viewSeesNewCard") {
CHECK(vm.callFunc(vm.findFunc("testCardCppView"), vm).toBool());
}
TEST_CASE("card.cardStateName") {
CHECK_EQ(std::string(cardStateName(CardState::Deck)), std::string("deck"));
CHECK_EQ(std::string(cardStateName(CardState::Hand)), std::string("hand"));
CHECK_EQ(std::string(cardStateName(CardState::Hovered)), std::string("hovered"));
CHECK_EQ(std::string(cardStateName(CardState::Dragging)), std::string("dragging"));
CHECK_EQ(std::string(cardStateName(CardState::Returning)), std::string("returning"));
CHECK_EQ(std::string(cardStateName(CardState::Played)), std::string("played"));
CHECK_EQ(std::string(cardStateName(CardState::Discarded)), std::string("discarded"));
CHECK_EQ(std::string(cardStateName(CardState::Disabled)), std::string("disabled"));
}
TEST_CASE("card.cardData.hitAndDescribe") {
Card mod;
mod.registerCardsFromJson(kDefs);
CardData *c = mod.newCard("flame");
c->layout()->x = 100.f;
c->layout()->y = 100.f;
c->layout()->w = 50.f;
c->layout()->h = 70.f;
c->layout()->scale = 1.f;
CHECK(c->hit(100.f, 100.f));
CHECK(c->hit(125.f, 135.f));
CHECK(!c->hit(126.f, 100.f));
CHECK(!c->hit(100.f, 136.f));
c->layout()->scale = 2.f; // enlarged hit box
CHECK(c->hit(140.f, 100.f));
c->layout()->w = 0.f; // zero-size never hits
CHECK(!c->hit(100.f, 100.f));
const std::string creature = c->describe();
CHECK(creature.find("Flame") != std::string::npos);
CHECK(creature.find("3/2") != std::string::npos);
CardData *spell = mod.newCard("bolt");
CHECK(spell->describe().find("\xe6\xb3\x95\xe6\x9c\xaf") != std::string::npos); // 法术
}
TEST_CASE("card.deck.drawPeekBoundsAndShuffle") {
Card mod;
mod.registerCardsFromJson(kDefs);
Deck *d = mod.newDeck();
CHECK(d->isEmpty());
CHECK_EQ(d->draw(), nullptr);
CHECK_EQ(d->peek(), nullptr);
CHECK_EQ(d->get(-1), nullptr);
CHECK_EQ(d->get(0), nullptr);
CardData *a = mod.newCard("flame");
CardData *b = mod.newCard("bolt");
d->push(a);
d->push(b);
CHECK_EQ(d->get(0), a);
CHECK_EQ(d->get(1), b);
CHECK_EQ(d->get(2), nullptr);
d->shuffle(); // membership preserved, order may change
CHECK_EQ(d->count(), 2);
const bool top0 = d->get(0) == a || d->get(0) == b;
const bool top1 = d->get(1) == a || d->get(1) == b;
CHECK(top0);
CHECK(top1);
CHECK(d->get(0) != d->get(1));
d->clear();
CHECK(d->isEmpty());
}
TEST_CASE("card.zone.containsAndAccepts") {
Card mod;
mod.registerCardsFromJson(kDefs);
Zone *z = mod.newZone("board", "Board", 10.f, 20.f, 100.f, 60.f);
CHECK(z->contains(10.f, 20.f));
CHECK(z->contains(109.f, 79.f));
CHECK(!z->contains(9.f, 20.f));
CHECK(!z->contains(111.f, 81.f)); // rect bounds are inclusive
CardData *flame = mod.newCard("flame");
CardData *bolt = mod.newCard("bolt");
CHECK(z->accepts(flame));
CHECK(z->accepts(bolt));
CHECK(!z->accepts(nullptr));
z->filter()->acceptKinds = {"spell"};
CHECK(!z->accepts(flame));
CHECK(z->accepts(bolt));
z->rect()->enabled = false;
CHECK(!z->accepts(bolt));
}
TEST_CASE("card.hand.addCardSetsLayoutAndPhase") {
Card mod;
mod.registerCardsFromJson(kDefs);
LayoutConfig *cfg = mod.newConfig();
Hand *h = mod.newHand(cfg);
CardData *flame = mod.newCard("flame");
h->addCard(flame);
CHECK(static_cast<int>(flame->state()->phase) == static_cast<int>(CardState::Hand));
CHECK_EQ(flame->layout()->w, cfg->cardW);
CHECK(std::fabs(flame->layout()->scale - 0.01f) < 1e-5f);
CardData *bolt = mod.newCard("bolt");
bolt->visual()->disabled = true;
h->addCard(bolt);
CHECK(static_cast<int>(bolt->state()->phase) == static_cast<int>(CardState::Disabled));
CHECK(std::fabs(bolt->layout()->alpha - cfg->disabledAlpha) < 1e-5f);
CardData *other = mod.newCard("flame");
CHECK(!h->removeCard(other));
CHECK(h->removeCard(flame));
CHECK_EQ(h->count(), 1);
CHECK_EQ(h->find(bolt->identity()->id), bolt);
CHECK_EQ(h->find("nope"), nullptr);
CHECK_EQ(h->get(5), nullptr);
h->clear();
CHECK_EQ(h->count(), 0);
}
TEST_CASE("card.hand.pickTopmostAndFanSlot") {
Card mod;
mod.registerCardsFromJson(kDefs);
LayoutConfig *cfg = mod.newConfig();
Hand *h = mod.newHand(cfg);
CardData *a = mod.newCard("flame");
CardData *b = mod.newCard("bolt");
h->addCard(a);
h->addCard(b);
a->layout()->x = 100.f;
a->layout()->y = 100.f;
a->layout()->w = 50.f;
a->layout()->h = 70.f;
a->layout()->scale = 1.f;
b->layout()->x = 100.f;
b->layout()->y = 100.f;
b->layout()->w = 50.f;
b->layout()->h = 70.f;
b->layout()->scale = 1.f;
CHECK_EQ(h->pick(100.f, 100.f), b); // top-most = last added
CHECK_EQ(h->pick(300.f, 300.f), nullptr);
float ox = 0.f, oy = 0.f, ang = 0.f;
h->slotTransform(0, 1, ox, oy, ang);
CHECK(std::fabs(ox - cfg->handX) < 1e-3f);
CHECK(std::fabs(oy - cfg->handY) < 1e-3f);
CHECK(std::fabs(ang) < 1e-4f);
h->slotTransform(0, 3, ox, oy, ang);
CHECK(ang < 0.f);
h->slotTransform(1, 3, ox, oy, ang);
CHECK(std::fabs(ang) < 1e-4f);
h->slotTransform(2, 3, ox, oy, ang);
CHECK(ang > 0.f);
}
namespace {
void settleHand(Hand *h, const std::vector<Zone *> &zones, std::vector<CardEvent> &out) {
for (int i = 0; i < 120; ++i) h->update(1.f / 60.f, 0.f, 0.f, false, zones, out);
out.clear();
}
} // namespace
TEST_CASE("card.hand.updateClickDragDrop") {
Card mod;
mod.registerCardsFromJson(kDefs);
LayoutConfig *cfg = mod.newConfig();
cfg->dragThreshold = 4.f;
cfg->cardW = 50.f;
cfg->cardH = 70.f;
cfg->handX = 100.f;
cfg->handY = 100.f;
cfg->spacing = 20.f;
cfg->arcHeight = 0.f;
cfg->rotationAngle = 0.f;
Hand *h = mod.newHand(cfg);
h->meta()->owner = "p1";
CardData *a = mod.newCard("flame"); // creature
CardData *b = mod.newCard("bolt"); // spell
h->addCard(a);
h->addCard(b);
Zone *board = mod.newZone("board", "Board", 100.f, 300.f, 200.f, 60.f);
board->filter()->acceptKinds = {"creature"};
Zone *spellOnly = mod.newZone("magic", "Magic", 100.f, 400.f, 200.f, 60.f);
spellOnly->filter()->acceptKinds = {"spell"};
std::vector<Zone *> zones = {board, spellOnly};
std::vector<CardEvent> out;
settleHand(h, zones, out);
// After settling with two cards: a at (65,100), b at (135,100).
CHECK(near(a->layout()->x, 65.f, 0.5f));
CHECK(near(b->layout()->x, 135.f, 0.5f));
// Click: press + release without moving beyond threshold.
h->update(1.f / 60.f, 135.f, 100.f, true, zones, out);
CHECK_EQ(out.size(), 0u);
h->update(1.f / 60.f, 136.f, 100.f, false, zones, out);
REQUIRE_EQ(out.size(), 1u);
CHECK_EQ(out[0].type, std::string("click"));
CHECK_EQ(out[0].hand, std::string("p1"));
CHECK(out[0].cardId.find("bolt") == 0); // instance id is "bolt#<n>"
out.clear();
// Drag spell onto the creature-only board -> dropRejected.
h->update(1.f / 60.f, 135.f, 100.f, true, zones, out);
h->update(1.f / 60.f, 145.f, 100.f, true, zones, out);
CHECK_EQ(b->state()->dragging, true);
h->update(1.f / 60.f, 145.f, 330.f, true, zones, out);
h->update(1.f / 60.f, 145.f, 330.f, false, zones, out);
REQUIRE_EQ(out.size(), 1u);
CHECK_EQ(out[0].type, std::string("dropRejected"));
CHECK_EQ(out[0].zoneId, std::string("board"));
CHECK_EQ(out[0].reason, std::string("kind_not_allowed"));
CHECK(static_cast<int>(b->state()->phase) == static_cast<int>(CardState::Returning));
out.clear();
settleHand(h, zones, out);
// Drag spell onto the spell-only zone -> drop accepted.
h->update(1.f / 60.f, 135.f, 100.f, true, zones, out);
h->update(1.f / 60.f, 145.f, 100.f, true, zones, out);
h->update(1.f / 60.f, 145.f, 430.f, true, zones, out);
h->update(1.f / 60.f, 145.f, 430.f, false, zones, out);
REQUIRE_EQ(out.size(), 1u);
CHECK_EQ(out[0].type, std::string("drop"));
CHECK_EQ(out[0].zoneId, std::string("magic"));
out.clear();
settleHand(h, zones, out);
// Non-interactive hand: press produces no click/drag events.
h->meta()->interactive = false;
h->update(1.f / 60.f, 135.f, 100.f, true, zones, out);
h->update(1.f / 60.f, 136.f, 100.f, false, zones, out);
CHECK_EQ(out.size(), 0u);
}